diff --git a/internal/db/db.go b/internal/db/db.go index 0d71bfa..b92e9bd 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -36,10 +36,10 @@ func GetDbMap(db *sql.DB, dbtype config.DbType) (*gorp.DbMap, error) { dbmap := &gorp.DbMap{Db: db, Dialect: dialect} dbmap.AddTableWithName(models.User{}, "users").SetKeys(true, "UserId") dbmap.AddTableWithName(models.Session{}, "sessions").SetKeys(true, "SessionId") - dbmap.AddTableWithName(handlers.Account{}, "accounts").SetKeys(true, "AccountId") + dbmap.AddTableWithName(models.Account{}, "accounts").SetKeys(true, "AccountId") dbmap.AddTableWithName(models.Security{}, "securities").SetKeys(true, "SecurityId") - dbmap.AddTableWithName(handlers.Transaction{}, "transactions").SetKeys(true, "TransactionId") - dbmap.AddTableWithName(handlers.Split{}, "splits").SetKeys(true, "SplitId") + dbmap.AddTableWithName(models.Transaction{}, "transactions").SetKeys(true, "TransactionId") + dbmap.AddTableWithName(models.Split{}, "splits").SetKeys(true, "SplitId") dbmap.AddTableWithName(handlers.Price{}, "prices").SetKeys(true, "PriceId") rtable := dbmap.AddTableWithName(handlers.Report{}, "reports").SetKeys(true, "ReportId") rtable.ColMap("Lua").SetMaxSize(handlers.LuaMaxLength + luaMaxLengthBuffer) diff --git a/internal/handlers/accounts.go b/internal/handlers/accounts.go index dd3fa14..2812fb4 100644 --- a/internal/handlers/accounts.go +++ b/internal/handlers/accounts.go @@ -1,127 +1,14 @@ package handlers import ( - "encoding/json" "errors" "github.com/aclindsa/moneygo/internal/models" "log" "net/http" - "strings" ) -type AccountType int64 - -const ( - Bank AccountType = 1 // start at 1 so that the default (0) is invalid - Cash = 2 - Asset = 3 - Liability = 4 - Investment = 5 - Income = 6 - Expense = 7 - Trading = 8 - Equity = 9 - Receivable = 10 - Payable = 11 -) - -var AccountTypes = []AccountType{ - Bank, - Cash, - Asset, - Liability, - Investment, - Income, - Expense, - Trading, - Equity, - Receivable, - Payable, -} - -func (t AccountType) String() string { - switch t { - case Bank: - return "Bank" - case Cash: - return "Cash" - case Asset: - return "Asset" - case Liability: - return "Liability" - case Investment: - return "Investment" - case Income: - return "Income" - case Expense: - return "Expense" - case Trading: - return "Trading" - case Equity: - return "Equity" - case Receivable: - return "Receivable" - case Payable: - return "Payable" - } - return "" -} - -type Account struct { - AccountId int64 - ExternalAccountId string - UserId int64 - SecurityId int64 - ParentAccountId int64 // -1 if this account is at the root - Type AccountType - Name string - - // monotonically-increasing account transaction version number. Used for - // allowing a client to ensure they have a consistent version when paging - // through transactions. - AccountVersion int64 `json:"Version"` - - // Optional fields specifying how to fetch transactions from a bank via OFX - OFXURL string - OFXORG string - OFXFID string - OFXUser string - OFXBankID string // OFX BankID (BrokerID if AcctType == Investment) - OFXAcctID string - OFXAcctType string // ofxgo.acctType - OFXClientUID string - OFXAppID string - OFXAppVer string - OFXVersion string - OFXNoIndent bool -} - -type AccountList struct { - Accounts *[]Account `json:"accounts"` -} - -func (a *Account) Write(w http.ResponseWriter) error { - enc := json.NewEncoder(w) - return enc.Encode(a) -} - -func (a *Account) Read(json_str string) error { - dec := json.NewDecoder(strings.NewReader(json_str)) - return dec.Decode(a) -} - -func (al *AccountList) Write(w http.ResponseWriter) error { - enc := json.NewEncoder(w) - return enc.Encode(al) -} - -func (al *AccountList) Read(json_str string) error { - dec := json.NewDecoder(strings.NewReader(json_str)) - return dec.Decode(al) -} - -func GetAccount(tx *Tx, accountid int64, userid int64) (*Account, error) { - var a Account +func GetAccount(tx *Tx, accountid int64, userid int64) (*models.Account, error) { + var a models.Account err := tx.SelectOne(&a, "SELECT * from accounts where UserId=? AND AccountId=?", userid, accountid) if err != nil { @@ -130,8 +17,8 @@ func GetAccount(tx *Tx, accountid int64, userid int64) (*Account, error) { return &a, nil } -func GetAccounts(tx *Tx, userid int64) (*[]Account, error) { - var accounts []Account +func GetAccounts(tx *Tx, userid int64) (*[]models.Account, error) { + var accounts []models.Account _, err := tx.Select(&accounts, "SELECT * from accounts where UserId=?", userid) if err != nil { @@ -142,9 +29,9 @@ func GetAccounts(tx *Tx, userid int64) (*[]Account, error) { // Get (and attempt to create if it doesn't exist). Matches on UserId, // SecurityId, Type, Name, and ParentAccountId -func GetCreateAccount(tx *Tx, a Account) (*Account, error) { - var accounts []Account - var account Account +func GetCreateAccount(tx *Tx, a models.Account) (*models.Account, error) { + var accounts []models.Account + var account models.Account // Try to find the top-level trading account _, err := tx.Select(&accounts, "SELECT * from accounts where UserId=? AND SecurityId=? AND Type=? AND Name=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1", a.UserId, a.SecurityId, a.Type, a.Name, a.ParentAccountId) @@ -170,9 +57,9 @@ func GetCreateAccount(tx *Tx, a Account) (*Account, error) { // Get (and attempt to create if it doesn't exist) the security/currency // trading account for the supplied security/currency -func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error) { - var tradingAccount Account - var account Account +func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*models.Account, error) { + var tradingAccount models.Account + var account models.Account user, err := GetUser(tx, userid) if err != nil { @@ -180,7 +67,7 @@ func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error) } tradingAccount.UserId = userid - tradingAccount.Type = Trading + tradingAccount.Type = models.Trading tradingAccount.Name = "Trading" tradingAccount.SecurityId = user.DefaultCurrency tradingAccount.ParentAccountId = -1 @@ -200,7 +87,7 @@ func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error) account.Name = security.Name account.ParentAccountId = ta.AccountId account.SecurityId = securityid - account.Type = Trading + account.Type = models.Trading a, err := GetCreateAccount(tx, account) if err != nil { @@ -212,9 +99,9 @@ func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error) // Get (and attempt to create if it doesn't exist) the security/currency // imbalance account for the supplied security/currency -func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*Account, error) { - var imbalanceAccount Account - var account Account +func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*models.Account, error) { + var imbalanceAccount models.Account + var account models.Account xxxtemplate := FindSecurityTemplate("XXX", models.Currency) if xxxtemplate == nil { return nil, errors.New("Couldn't find XXX security template") @@ -228,7 +115,7 @@ func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*Account, erro imbalanceAccount.Name = "Imbalances" imbalanceAccount.ParentAccountId = -1 imbalanceAccount.SecurityId = xxxsecurity.SecurityId - imbalanceAccount.Type = Bank + imbalanceAccount.Type = models.Bank // Find/create the top-level trading account ia, err := GetCreateAccount(tx, imbalanceAccount) @@ -245,7 +132,7 @@ func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*Account, erro account.Name = security.Name account.ParentAccountId = ia.AccountId account.SecurityId = securityid - account.Type = Bank + account.Type = models.Bank a, err := GetCreateAccount(tx, account) if err != nil { @@ -273,7 +160,7 @@ func (cae CircularAccountsError) Error() string { return "Would result in circular account relationship" } -func insertUpdateAccount(tx *Tx, a *Account, insert bool) error { +func insertUpdateAccount(tx *Tx, a *models.Account, insert bool) error { found := make(map[int64]bool) if !insert { found[a.AccountId] = true @@ -286,7 +173,7 @@ func insertUpdateAccount(tx *Tx, a *Account, insert bool) error { return TooMuchNestingError{} } - var a Account + var a models.Account err := tx.SelectOne(&a, "SELECT * from accounts where AccountId=?", parentid) if err != nil { return ParentAccountMissingError{} @@ -329,15 +216,15 @@ func insertUpdateAccount(tx *Tx, a *Account, insert bool) error { return nil } -func InsertAccount(tx *Tx, a *Account) error { +func InsertAccount(tx *Tx, a *models.Account) error { return insertUpdateAccount(tx, a, true) } -func UpdateAccount(tx *Tx, a *Account) error { +func UpdateAccount(tx *Tx, a *models.Account) error { return insertUpdateAccount(tx, a, false) } -func DeleteAccount(tx *Tx, a *Account) error { +func DeleteAccount(tx *Tx, a *models.Account) error { if a.ParentAccountId != -1 { // Re-parent splits to this account's parent account if this account isn't a root account _, err := tx.Exec("UPDATE splits SET AccountId=? WHERE AccountId=?", a.ParentAccountId, a.AccountId) @@ -384,7 +271,7 @@ func AccountHandler(r *http.Request, context *Context) ResponseWriterWriter { return AccountImportHandler(context, r, user, accountid) } - var account Account + var account models.Account if err := ReadJSON(r, &account); err != nil { return NewError(3 /*Invalid Request*/) } @@ -415,7 +302,7 @@ func AccountHandler(r *http.Request, context *Context) ResponseWriterWriter { } else if r.Method == "GET" { if context.LastLevel() { //Return all Accounts - var al AccountList + var al models.AccountList accounts, err := GetAccounts(context.Tx, user.UserId) if err != nil { log.Print(err) @@ -447,7 +334,7 @@ func AccountHandler(r *http.Request, context *Context) ResponseWriterWriter { return NewError(3 /*Invalid Request*/) } if r.Method == "PUT" { - var account Account + var account models.Account if err := ReadJSON(r, &account); err != nil || account.AccountId != accountid { return NewError(3 /*Invalid Request*/) } diff --git a/internal/handlers/accounts_lua.go b/internal/handlers/accounts_lua.go index 8a4d5db..5a2fc23 100644 --- a/internal/handlers/accounts_lua.go +++ b/internal/handlers/accounts_lua.go @@ -11,8 +11,8 @@ import ( const luaAccountTypeName = "account" -func luaContextGetAccounts(L *lua.LState) (map[int64]*Account, error) { - var account_map map[int64]*Account +func luaContextGetAccounts(L *lua.LState) (map[int64]*models.Account, error) { + var account_map map[int64]*models.Account ctx := L.Context() @@ -21,7 +21,7 @@ func luaContextGetAccounts(L *lua.LState) (map[int64]*Account, error) { return nil, errors.New("Couldn't find tx in lua's Context") } - account_map, ok = ctx.Value(accountsContextKey).(map[int64]*Account) + account_map, ok = ctx.Value(accountsContextKey).(map[int64]*models.Account) if !ok { user, ok := ctx.Value(userContextKey).(*models.User) if !ok { @@ -33,7 +33,7 @@ func luaContextGetAccounts(L *lua.LState) (map[int64]*Account, error) { return nil, err } - account_map = make(map[int64]*Account) + account_map = make(map[int64]*models.Account) for i := range *accounts { account_map[(*accounts)[i].AccountId] = &(*accounts)[i] } @@ -69,7 +69,7 @@ func luaRegisterAccounts(L *lua.LState) { L.SetField(mt, "__eq", L.NewFunction(luaAccount__eq)) L.SetField(mt, "__metatable", lua.LString("protected")) - for _, accttype := range AccountTypes { + for _, accttype := range models.AccountTypes { L.SetField(mt, accttype.String(), lua.LNumber(float64(accttype))) } @@ -79,7 +79,7 @@ func luaRegisterAccounts(L *lua.LState) { L.SetGlobal("get_accounts", getAccountsFn) } -func AccountToLua(L *lua.LState, account *Account) *lua.LUserData { +func AccountToLua(L *lua.LState, account *models.Account) *lua.LUserData { ud := L.NewUserData() ud.Value = account L.SetMetatable(ud, L.GetTypeMetatable(luaAccountTypeName)) @@ -87,9 +87,9 @@ func AccountToLua(L *lua.LState, account *Account) *lua.LUserData { } // Checks whether the first lua argument is a *LUserData with *Account and returns this *Account. -func luaCheckAccount(L *lua.LState, n int) *Account { +func luaCheckAccount(L *lua.LState, n int) *models.Account { ud := L.CheckUserData(n) - if account, ok := ud.Value.(*Account); ok { + if account, ok := ud.Value.(*models.Account); ok { return account } L.ArgError(n, "account expected") diff --git a/internal/handlers/accounts_test.go b/internal/handlers/accounts_test.go index 0abd029..d0b92a4 100644 --- a/internal/handlers/accounts_test.go +++ b/internal/handlers/accounts_test.go @@ -2,19 +2,20 @@ package handlers_test import ( "github.com/aclindsa/moneygo/internal/handlers" + "github.com/aclindsa/moneygo/internal/models" "net/http" "strconv" "testing" ) -func createAccount(client *http.Client, account *handlers.Account) (*handlers.Account, error) { - var a handlers.Account +func createAccount(client *http.Client, account *models.Account) (*models.Account, error) { + var a models.Account err := create(client, account, &a, "/v1/accounts/") return &a, err } -func getAccount(client *http.Client, accountid int64) (*handlers.Account, error) { - var a handlers.Account +func getAccount(client *http.Client, accountid int64) (*models.Account, error) { + var a models.Account err := read(client, &a, "/v1/accounts/"+strconv.FormatInt(accountid, 10)) if err != nil { return nil, err @@ -22,8 +23,8 @@ func getAccount(client *http.Client, accountid int64) (*handlers.Account, error) return &a, nil } -func getAccounts(client *http.Client) (*handlers.AccountList, error) { - var al handlers.AccountList +func getAccounts(client *http.Client) (*models.AccountList, error) { + var al models.AccountList err := read(client, &al, "/v1/accounts/") if err != nil { return nil, err @@ -31,8 +32,8 @@ func getAccounts(client *http.Client) (*handlers.AccountList, error) { return &al, nil } -func updateAccount(client *http.Client, account *handlers.Account) (*handlers.Account, error) { - var a handlers.Account +func updateAccount(client *http.Client, account *models.Account) (*models.Account, error) { + var a models.Account err := update(client, account, &a, "/v1/accounts/"+strconv.FormatInt(account.AccountId, 10)) if err != nil { return nil, err @@ -40,7 +41,7 @@ func updateAccount(client *http.Client, account *handlers.Account) (*handlers.Ac return &a, nil } -func deleteAccount(client *http.Client, a *handlers.Account) error { +func deleteAccount(client *http.Client, a *models.Account) error { err := remove(client, "/v1/accounts/"+strconv.FormatInt(a.AccountId, 10)) if err != nil { return err @@ -137,7 +138,7 @@ func TestUpdateAccount(t *testing.T) { curr := d.accounts[i] curr.Name = "blah" - curr.Type = handlers.Payable + curr.Type = models.Payable for _, s := range d.securities { if s.UserId == curr.UserId { curr.SecurityId = s.SecurityId diff --git a/internal/handlers/common_test.go b/internal/handlers/common_test.go index 5ff8c02..a0ef7f8 100644 --- a/internal/handlers/common_test.go +++ b/internal/handlers/common_test.go @@ -7,6 +7,7 @@ import ( "github.com/aclindsa/moneygo/internal/config" "github.com/aclindsa/moneygo/internal/db" "github.com/aclindsa/moneygo/internal/handlers" + "github.com/aclindsa/moneygo/internal/models" "io" "io/ioutil" "log" @@ -202,7 +203,7 @@ func uploadFile(client *http.Client, filename, urlsuffix string) error { return nil } -func accountBalanceHelper(t *testing.T, client *http.Client, account *handlers.Account, balance string) { +func accountBalanceHelper(t *testing.T, client *http.Client, account *models.Account, balance string) { t.Helper() transactions, err := getAccountTransactions(client, account.AccountId, 0, 0, "") if err != nil { diff --git a/internal/handlers/gnucash.go b/internal/handlers/gnucash.go index 9670f1e..f99978e 100644 --- a/internal/handlers/gnucash.go +++ b/internal/handlers/gnucash.go @@ -127,8 +127,8 @@ type GnucashXMLImport struct { type GnucashImport struct { Securities []models.Security - Accounts []Account - Transactions []Transaction + Accounts []models.Account + Transactions []models.Transaction Prices []Price } @@ -206,7 +206,7 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) { //Translate to our account format, figuring out parent relationships for guid := range accountMap { ga := accountMap[guid] - var a Account + var a models.Account a.AccountId = ga.accountid if ga.ParentAccountId == rootAccount.AccountId { @@ -229,29 +229,29 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) { //TODO find account types switch ga.Type { default: - a.Type = Bank + a.Type = models.Bank case "ASSET": - a.Type = Asset + a.Type = models.Asset case "BANK": - a.Type = Bank + a.Type = models.Bank case "CASH": - a.Type = Cash + a.Type = models.Cash case "CREDIT", "LIABILITY": - a.Type = Liability + a.Type = models.Liability case "EQUITY": - a.Type = Equity + a.Type = models.Equity case "EXPENSE": - a.Type = Expense + a.Type = models.Expense case "INCOME": - a.Type = Income + a.Type = models.Income case "PAYABLE": - a.Type = Payable + a.Type = models.Payable case "RECEIVABLE": - a.Type = Receivable + a.Type = models.Receivable case "MUTUAL", "STOCK": - a.Type = Investment + a.Type = models.Investment case "TRADING": - a.Type = Trading + a.Type = models.Trading } gncimport.Accounts = append(gncimport.Accounts, a) @@ -261,20 +261,20 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) { for i := range gncxml.Transactions { gt := gncxml.Transactions[i] - t := new(Transaction) + t := new(models.Transaction) t.Description = gt.Description t.Date = gt.DatePosted.Date.Time for j := range gt.Splits { gs := gt.Splits[j] - s := new(Split) + s := new(models.Split) switch gs.Status { default: // 'n', or not present - s.Status = Imported + s.Status = models.Imported case "c": - s.Status = Cleared + s.Status = models.Cleared case "y": - s.Status = Reconciled + s.Status = models.Reconciled } account, ok := accountMap[gs.AccountId] @@ -437,7 +437,7 @@ func GnucashImportHandler(r *http.Request, context *Context) ResponseWriterWrite } split.AccountId = acctId - exists, err := split.AlreadyImported(context.Tx) + exists, err := SplitAlreadyImported(context.Tx, split) if err != nil { log.Print("Error checking if split was already imported:", err) return NewError(999 /*Internal Error*/) diff --git a/internal/handlers/gnucash_test.go b/internal/handlers/gnucash_test.go index 7eacc03..960078f 100644 --- a/internal/handlers/gnucash_test.go +++ b/internal/handlers/gnucash_test.go @@ -1,7 +1,6 @@ package handlers_test import ( - "github.com/aclindsa/moneygo/internal/handlers" "github.com/aclindsa/moneygo/internal/models" "net/http" "testing" @@ -32,19 +31,19 @@ func TestImportGnucash(t *testing.T) { } // Next, find the Expenses/Groceries account and verify it's balance - var income, equity, liabilities, expenses, salary, creditcard, groceries, cable, openingbalances *handlers.Account + var income, equity, liabilities, expenses, salary, creditcard, groceries, cable, openingbalances *models.Account accounts, err := getAccounts(d.clients[0]) if err != nil { t.Fatalf("Error fetching accounts: %s\n", err) } for i, account := range *accounts.Accounts { - if account.Name == "Income" && account.Type == handlers.Income && account.ParentAccountId == -1 { + if account.Name == "Income" && account.Type == models.Income && account.ParentAccountId == -1 { income = &(*accounts.Accounts)[i] - } else if account.Name == "Equity" && account.Type == handlers.Equity && account.ParentAccountId == -1 { + } else if account.Name == "Equity" && account.Type == models.Equity && account.ParentAccountId == -1 { equity = &(*accounts.Accounts)[i] - } else if account.Name == "Liabilities" && account.Type == handlers.Liability && account.ParentAccountId == -1 { + } else if account.Name == "Liabilities" && account.Type == models.Liability && account.ParentAccountId == -1 { liabilities = &(*accounts.Accounts)[i] - } else if account.Name == "Expenses" && account.Type == handlers.Expense && account.ParentAccountId == -1 { + } else if account.Name == "Expenses" && account.Type == models.Expense && account.ParentAccountId == -1 { expenses = &(*accounts.Accounts)[i] } } @@ -61,15 +60,15 @@ func TestImportGnucash(t *testing.T) { t.Fatalf("Couldn't find 'Expenses' account") } for i, account := range *accounts.Accounts { - if account.Name == "Salary" && account.Type == handlers.Income && account.ParentAccountId == income.AccountId { + if account.Name == "Salary" && account.Type == models.Income && account.ParentAccountId == income.AccountId { salary = &(*accounts.Accounts)[i] - } else if account.Name == "Opening Balances" && account.Type == handlers.Equity && account.ParentAccountId == equity.AccountId { + } else if account.Name == "Opening Balances" && account.Type == models.Equity && account.ParentAccountId == equity.AccountId { openingbalances = &(*accounts.Accounts)[i] - } else if account.Name == "Credit Card" && account.Type == handlers.Liability && account.ParentAccountId == liabilities.AccountId { + } else if account.Name == "Credit Card" && account.Type == models.Liability && account.ParentAccountId == liabilities.AccountId { creditcard = &(*accounts.Accounts)[i] - } else if account.Name == "Groceries" && account.Type == handlers.Expense && account.ParentAccountId == expenses.AccountId { + } else if account.Name == "Groceries" && account.Type == models.Expense && account.ParentAccountId == expenses.AccountId { groceries = &(*accounts.Accounts)[i] - } else if account.Name == "Cable" && account.Type == handlers.Expense && account.ParentAccountId == expenses.AccountId { + } else if account.Name == "Cable" && account.Type == models.Expense && account.ParentAccountId == expenses.AccountId { cable = &(*accounts.Accounts)[i] } } diff --git a/internal/handlers/imports.go b/internal/handlers/imports.go index 442c4e3..78d5236 100644 --- a/internal/handlers/imports.go +++ b/internal/handlers/imports.go @@ -78,7 +78,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re // TODO Ensure all transactions have at least one split in the account // we're importing to? - var transactions []Transaction + var transactions []models.Transaction for _, transaction := range itl.Transactions { transaction.UserId = user.UserId @@ -91,7 +91,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re // and fixup the SecurityId to be a valid one for this user's actual // securities instead of a placeholder from the import for _, split := range transaction.Splits { - split.Status = Imported + split.Status = models.Imported if split.AccountId != -1 { if split.AccountId != importedAccount.AccountId { log.Print("Imported split's AccountId wasn't -1 but also didn't match the account") @@ -101,7 +101,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re } else if split.SecurityId != -1 { if sec, ok := securitymap[split.SecurityId]; ok { // TODO try to auto-match splits to existing accounts based on past transactions that look like this one - if split.ImportSplitType == TradingAccount { + if split.ImportSplitType == models.TradingAccount { // Find/make trading account if we're that type of split trading_account, err := GetTradingAccount(tx, user.UserId, sec.SecurityId) if err != nil { @@ -110,8 +110,8 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re } split.AccountId = trading_account.AccountId split.SecurityId = -1 - } else if split.ImportSplitType == SubAccount { - subaccount := &Account{ + } else if split.ImportSplitType == models.SubAccount { + subaccount := &models.Account{ UserId: user.UserId, Name: sec.Name, ParentAccountId: account.AccountId, @@ -138,7 +138,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re } } - imbalances, err := transaction.GetImbalances(tx) + imbalances, err := GetTransactionImbalances(tx, &transaction) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) @@ -155,7 +155,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re } // Add new split to fixup imbalance - split := new(Split) + split := new(models.Split) r := new(big.Rat) r.Neg(&imbalance) security, err := GetSecurity(tx, imbalanced_security, user.UserId) @@ -186,7 +186,7 @@ func ofxImportHelper(tx *Tx, r io.Reader, user *models.User, accountid int64) Re split.SecurityId = -1 } - exists, err := split.AlreadyImported(tx) + exists, err := SplitAlreadyImported(tx, split) if err != nil { log.Print("Error checking if split was already imported:", err) return NewError(999 /*Internal Error*/) @@ -251,7 +251,7 @@ func OFXImportHandler(context *Context, r *http.Request, user *models.User, acco return NewError(999 /*Internal Error*/) } - if account.Type == Investment { + if account.Type == models.Investment { // Investment account statementRequest := ofxgo.InvStatementRequest{ TrnUID: *transactionuid, diff --git a/internal/handlers/ofx.go b/internal/handlers/ofx.go index befd147..a183aab 100644 --- a/internal/handlers/ofx.go +++ b/internal/handlers/ofx.go @@ -11,8 +11,8 @@ import ( type OFXImport struct { Securities []models.Security - Accounts []Account - Transactions []Transaction + Accounts []models.Account + Transactions []models.Transaction // Balances map[int64]string // map AccountIDs to ending balances } @@ -51,8 +51,8 @@ func (i *OFXImport) GetAddCurrency(isoname string) (*models.Security, error) { return &security, nil } -func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *Account) error { - var t Transaction +func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *models.Account) error { + var t models.Transaction t.Date = tran.DtPosted.UTC() @@ -70,7 +70,7 @@ func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *Account) er } } - var s1, s2 Split + var s1, s2 models.Split if len(tran.ExtdName) > 0 { s1.Memo = tran.ExtdName.String() } @@ -94,15 +94,15 @@ func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *Account) er s1.RemoteId = "ofx:" + tran.FiTID.String() // TODO CorrectFiTID/CorrectAction? - s1.ImportSplitType = ImportAccount - s2.ImportSplitType = ExternalAccount + s1.ImportSplitType = models.ImportAccount + s2.ImportSplitType = models.ExternalAccount security := i.Securities[account.SecurityId-1] s1.Amount = amt.FloatString(security.Precision) s2.Amount = amt.Neg(amt).FloatString(security.Precision) - s1.Status = Imported - s2.Status = Imported + s1.Status = models.Imported + s2.Status = models.Imported s1.AccountId = account.AccountId s2.AccountId = -1 @@ -122,12 +122,12 @@ func (i *OFXImport) importOFXBank(stmt *ofxgo.StatementResponse) error { return err } - account := Account{ + account := models.Account{ AccountId: int64(len(i.Accounts) + 1), ExternalAccountId: stmt.BankAcctFrom.AcctID.String(), SecurityId: security.SecurityId, ParentAccountId: -1, - Type: Bank, + Type: models.Bank, } if stmt.BankTranList != nil { @@ -149,12 +149,12 @@ func (i *OFXImport) importOFXCC(stmt *ofxgo.CCStatementResponse) error { return err } - account := Account{ + account := models.Account{ AccountId: int64(len(i.Accounts) + 1), ExternalAccountId: stmt.CCAcctFrom.AcctID.String(), SecurityId: security.SecurityId, ParentAccountId: -1, - Type: Liability, + Type: models.Liability, } i.Accounts = append(i.Accounts, account) @@ -208,14 +208,14 @@ func (i *OFXImport) importSecurities(seclist *ofxgo.SecurityList) error { return nil } -func (i *OFXImport) GetInvTran(invtran *ofxgo.InvTran) Transaction { - var t Transaction +func (i *OFXImport) GetInvTran(invtran *ofxgo.InvTran) models.Transaction { + var t models.Transaction t.Description = string(invtran.Memo) t.Date = invtran.DtTrade.UTC() return t } -func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&buy.InvTran) security, err := i.GetSecurityAlternateId(string(buy.SecID.UniqueID), models.Stock) @@ -254,10 +254,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac } if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Commission, + Status: models.Imported, + ImportSplitType: models.Commission, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -266,10 +266,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac }) } if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Taxes, + Status: models.Imported, + ImportSplitType: models.Taxes, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -278,10 +278,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac }) } if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Fees, + Status: models.Imported, + ImportSplitType: models.Fees, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -290,10 +290,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac }) } if num := load.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Load, + Status: models.Imported, + ImportSplitType: models.Load, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -301,20 +301,20 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac Amount: load.FloatString(curdef.Precision), }) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), Memo: memo, Amount: total.FloatString(curdef.Precision), }) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -324,10 +324,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac var units big.Rat units.Abs(&buy.Units.Rat) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: SubAccount, + Status: models.Imported, + ImportSplitType: models.SubAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -335,10 +335,10 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac Amount: units.FloatString(security.Precision), }) units.Neg(&units) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + buy.InvTran.FiTID.String(), @@ -349,7 +349,7 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac return &t, nil } -func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&income.InvTran) security, err := i.GetSecurityAlternateId(string(income.SecID.UniqueID), models.Stock) @@ -370,10 +370,10 @@ func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security, total.Mul(&total, &income.Currency.CurRate.Rat) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + income.InvTran.FiTID.String(), @@ -381,10 +381,10 @@ func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security, Amount: total.FloatString(curdef.Precision), }) total.Neg(&total) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: IncomeAccount, + Status: models.Imported, + ImportSplitType: models.IncomeAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + income.InvTran.FiTID.String(), @@ -395,7 +395,7 @@ func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security, return &t, nil } -func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&expense.InvTran) security, err := i.GetSecurityAlternateId(string(expense.SecID.UniqueID), models.Stock) @@ -415,10 +415,10 @@ func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models. total.Mul(&total, &expense.Currency.CurRate.Rat) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + expense.InvTran.FiTID.String(), @@ -426,10 +426,10 @@ func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models. Amount: total.FloatString(curdef.Precision), }) total.Neg(&total) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ExpenseAccount, + Status: models.Imported, + ImportSplitType: models.ExpenseAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + expense.InvTran.FiTID.String(), @@ -440,7 +440,7 @@ func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models. return &t, nil } -func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&marginint.InvTran) memo := string(marginint.InvTran.Memo) @@ -454,10 +454,10 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde total.Mul(&total, &marginint.Currency.CurRate.Rat) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + marginint.InvTran.FiTID.String(), @@ -465,10 +465,10 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde Amount: total.FloatString(curdef.Precision), }) total.Neg(&total) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: IncomeAccount, + Status: models.Imported, + ImportSplitType: models.IncomeAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + marginint.InvTran.FiTID.String(), @@ -479,7 +479,7 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde return &t, nil } -func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&reinvest.InvTran) security, err := i.GetSecurityAlternateId(string(reinvest.SecID.UniqueID), models.Stock) @@ -518,10 +518,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec } if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Commission, + Status: models.Imported, + ImportSplitType: models.Commission, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -530,10 +530,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec }) } if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Taxes, + Status: models.Imported, + ImportSplitType: models.Taxes, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -542,10 +542,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec }) } if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Fees, + Status: models.Imported, + ImportSplitType: models.Fees, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -554,10 +554,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec }) } if num := load.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Load, + Status: models.Imported, + ImportSplitType: models.Load, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -565,10 +565,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec Amount: load.FloatString(curdef.Precision), }) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -576,10 +576,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec Amount: total.FloatString(curdef.Precision), }) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: IncomeAccount, + Status: models.Imported, + ImportSplitType: models.IncomeAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -587,20 +587,20 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec Amount: total.FloatString(curdef.Precision), }) total.Neg(&total) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), Memo: memo, Amount: total.FloatString(curdef.Precision), }) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -610,10 +610,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec var units big.Rat units.Abs(&reinvest.Units.Rat) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: SubAccount, + Status: models.Imported, + ImportSplitType: models.SubAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -621,10 +621,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec Amount: units.FloatString(security.Precision), }) units.Neg(&units) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), @@ -635,7 +635,7 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec return &t, nil } -func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&retofcap.InvTran) security, err := i.GetSecurityAlternateId(string(retofcap.SecID.UniqueID), models.Stock) @@ -655,10 +655,10 @@ func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Sec total.Mul(&total, &retofcap.Currency.CurRate.Rat) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + retofcap.InvTran.FiTID.String(), @@ -666,10 +666,10 @@ func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Sec Amount: total.FloatString(curdef.Precision), }) total.Neg(&total) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: IncomeAccount, + Status: models.Imported, + ImportSplitType: models.IncomeAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + retofcap.InvTran.FiTID.String(), @@ -680,7 +680,7 @@ func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Sec return &t, nil } -func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, account *Account) (*Transaction, error) { +func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&sell.InvTran) security, err := i.GetSecurityAlternateId(string(sell.SecID.UniqueID), models.Stock) @@ -722,10 +722,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, } if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Commission, + Status: models.Imported, + ImportSplitType: models.Commission, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -734,10 +734,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, }) } if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Taxes, + Status: models.Imported, + ImportSplitType: models.Taxes, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -746,10 +746,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, }) } if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Fees, + Status: models.Imported, + ImportSplitType: models.Fees, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -758,10 +758,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, }) } if num := load.Num(); !num.IsInt64() || num.Int64() != 0 { - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: Load, + Status: models.Imported, + ImportSplitType: models.Load, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -769,20 +769,20 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, Amount: load.FloatString(curdef.Precision), }) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ImportAccount, + Status: models.Imported, + ImportSplitType: models.ImportAccount, AccountId: account.AccountId, SecurityId: -1, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), Memo: memo, Amount: total.FloatString(curdef.Precision), }) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: curdef.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -792,10 +792,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, var units big.Rat units.Abs(&sell.Units.Rat) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: TradingAccount, + Status: models.Imported, + ImportSplitType: models.TradingAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -803,10 +803,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, Amount: units.FloatString(security.Precision), }) units.Neg(&units) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: SubAccount, + Status: models.Imported, + ImportSplitType: models.SubAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + sell.InvTran.FiTID.String(), @@ -817,7 +817,7 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security, return &t, nil } -func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account) (*Transaction, error) { +func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *models.Account) (*models.Transaction, error) { t := i.GetInvTran(&transfer.InvTran) security, err := i.GetSecurityAlternateId(string(transfer.SecID.UniqueID), models.Stock) @@ -834,10 +834,10 @@ func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account) units.Neg(&transfer.Units.Rat) } - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: SubAccount, + Status: models.Imported, + ImportSplitType: models.SubAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + transfer.InvTran.FiTID.String(), @@ -845,10 +845,10 @@ func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account) Amount: units.FloatString(security.Precision), }) units.Neg(&units) - t.Splits = append(t.Splits, &Split{ + t.Splits = append(t.Splits, &models.Split{ // TODO ReversalFiTID? - Status: Imported, - ImportSplitType: ExternalAccount, + Status: models.Imported, + ImportSplitType: models.ExternalAccount, AccountId: -1, SecurityId: security.SecurityId, RemoteId: "ofx:" + transfer.InvTran.FiTID.String(), @@ -859,12 +859,12 @@ func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account) return &t, nil } -func (i *OFXImport) AddInvTransaction(invtran *ofxgo.InvTransaction, account *Account, curdef *models.Security) error { +func (i *OFXImport) AddInvTransaction(invtran *ofxgo.InvTransaction, account *models.Account, curdef *models.Security) error { if curdef.SecurityId < 1 || curdef.SecurityId > int64(len(i.Securities)) { return errors.New("Internal error: security index not found in OFX import\n") } - var t *Transaction + var t *models.Transaction var err error if tran, ok := (*invtran).(ofxgo.BuyDebt); ok { t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account) @@ -926,12 +926,12 @@ func (i *OFXImport) importOFXInv(stmt *ofxgo.InvStatementResponse) error { return err } - account := Account{ + account := models.Account{ AccountId: int64(len(i.Accounts) + 1), ExternalAccountId: stmt.InvAcctFrom.AcctID.String(), SecurityId: security.SecurityId, ParentAccountId: -1, - Type: Investment, + Type: models.Investment, } i.Accounts = append(i.Accounts, account) diff --git a/internal/handlers/ofx_test.go b/internal/handlers/ofx_test.go index 11c3f04..4d78f25 100644 --- a/internal/handlers/ofx_test.go +++ b/internal/handlers/ofx_test.go @@ -2,7 +2,6 @@ package handlers_test import ( "fmt" - "github.com/aclindsa/moneygo/internal/handlers" "github.com/aclindsa/moneygo/internal/models" "net/http" "strconv" @@ -77,7 +76,7 @@ func findSecurity(client *http.Client, symbol string, tipe models.SecurityType) return nil, fmt.Errorf("Unable to find security: \"%s\"", symbol) } -func findAccount(client *http.Client, name string, tipe handlers.AccountType, securityid int64) (*handlers.Account, error) { +func findAccount(client *http.Client, name string, tipe models.AccountType, securityid int64) (*models.Account, error) { accounts, err := getAccounts(client) if err != nil { return nil, err @@ -105,11 +104,11 @@ func TestImportOFX401kMutualFunds(t *testing.T) { t.Fatalf("Error removing default security: %s\n", err) } - account := &handlers.Account{ + account := &models.Account{ SecurityId: d.securities[0].SecurityId, UserId: d.users[0].UserId, ParentAccountId: -1, - Type: handlers.Investment, + Type: models.Investment, Name: "401k", } @@ -130,14 +129,14 @@ func TestImportOFX401kMutualFunds(t *testing.T) { if err != nil { t.Fatalf("Error finding VANGUARD TARGET 2045 security: %s\n", err) } - tradingaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", handlers.Trading, security.SecurityId) + tradingaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", models.Trading, security.SecurityId) if err != nil { t.Fatalf("Error finding VANGUARD TARGET 2045 trading account: %s\n", err) } accountBalanceHelper(t, d.clients[0], tradingaccount, "-3.35400") // Ensure actual holding account was created and in the correct place - investmentaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", handlers.Investment, security.SecurityId) + investmentaccount, err := findAccount(d.clients[0], "VANGUARD TARGET 2045", models.Investment, security.SecurityId) if err != nil { t.Fatalf("Error finding VANGUARD TARGET 2045 investment account: %s\n", err) } @@ -164,11 +163,11 @@ func TestImportOFXBrokerage(t *testing.T) { } // Create the brokerage account - account := &handlers.Account{ + account := &models.Account{ SecurityId: d.securities[0].SecurityId, UserId: d.users[0].UserId, ParentAccountId: -1, - Type: handlers.Investment, + Type: models.Investment, Name: "Personal Brokerage", } @@ -185,7 +184,7 @@ func TestImportOFXBrokerage(t *testing.T) { // Make sure the USD trading account was created and has the right // value - usdtrading, err := findAccount(d.clients[0], "USD", handlers.Trading, d.users[0].DefaultCurrency) + usdtrading, err := findAccount(d.clients[0], "USD", models.Trading, d.users[0].DefaultCurrency) if err != nil { t.Fatalf("Error finding USD trading account: %s\n", err) } @@ -210,14 +209,14 @@ func TestImportOFXBrokerage(t *testing.T) { t.Fatalf("Error finding security: %s\n", err) } - account, err := findAccount(d.clients[0], check.Name, handlers.Investment, security.SecurityId) + account, err := findAccount(d.clients[0], check.Name, models.Investment, security.SecurityId) if err != nil { t.Fatalf("Error finding trading account: %s\n", err) } accountBalanceHelper(t, d.clients[0], account, check.Balance) - tradingaccount, err := findAccount(d.clients[0], check.Name, handlers.Trading, security.SecurityId) + tradingaccount, err := findAccount(d.clients[0], check.Name, models.Trading, security.SecurityId) if err != nil { t.Fatalf("Error finding trading account: %s\n", err) } diff --git a/internal/handlers/prices_lua.go b/internal/handlers/prices_lua.go index 0c3fe89..8450319 100644 --- a/internal/handlers/prices_lua.go +++ b/internal/handlers/prices_lua.go @@ -1,6 +1,7 @@ package handlers import ( + "github.com/aclindsa/moneygo/internal/models" "github.com/yuin/gopher-lua" ) @@ -59,7 +60,7 @@ func luaPrice__index(L *lua.LState) int { } L.Push(SecurityToLua(L, c)) case "Value", "value": - amt, err := GetBigAmount(p.Value) + amt, err := models.GetBigAmount(p.Value) if err != nil { panic(err) } diff --git a/internal/handlers/testdata_test.go b/internal/handlers/testdata_test.go index c01544d..2cef0cd 100644 --- a/internal/handlers/testdata_test.go +++ b/internal/handlers/testdata_test.go @@ -39,8 +39,8 @@ type TestData struct { clients []*http.Client securities []models.Security prices []handlers.Price - accounts []handlers.Account // accounts must appear after their parents in this slice - transactions []handlers.Transaction + accounts []models.Account // accounts must appear after their parents in this slice + transactions []models.Transaction reports []handlers.Report tabulations []handlers.Tabulation } @@ -113,7 +113,7 @@ func (t *TestData) Initialize() (*TestData, error) { } for i, transaction := range t.transactions { - transaction.Splits = []*handlers.Split{} + transaction.Splits = []*models.Split{} for _, s := range t.transactions[i].Splits { // Make a copy of the split since Splits is a slice of pointers so // copying the transaction doesn't @@ -246,78 +246,78 @@ var data = []TestData{ RemoteId: "USDEUR819298714", }, }, - accounts: []handlers.Account{ + accounts: []models.Account{ { UserId: 0, SecurityId: 0, ParentAccountId: -1, - Type: handlers.Asset, + Type: models.Asset, Name: "Assets", }, { UserId: 0, SecurityId: 0, ParentAccountId: 0, - Type: handlers.Bank, + Type: models.Bank, Name: "Credit Union Checking", }, { UserId: 0, SecurityId: 0, ParentAccountId: -1, - Type: handlers.Expense, + Type: models.Expense, Name: "Expenses", }, { UserId: 0, SecurityId: 0, ParentAccountId: 2, - Type: handlers.Expense, + Type: models.Expense, Name: "Groceries", }, { UserId: 0, SecurityId: 0, ParentAccountId: 2, - Type: handlers.Expense, + Type: models.Expense, Name: "Cable", }, { UserId: 1, SecurityId: 2, ParentAccountId: -1, - Type: handlers.Asset, + Type: models.Asset, Name: "Assets", }, { UserId: 1, SecurityId: 2, ParentAccountId: -1, - Type: handlers.Expense, + Type: models.Expense, Name: "Expenses", }, { UserId: 0, SecurityId: 0, ParentAccountId: -1, - Type: handlers.Liability, + Type: models.Liability, Name: "Credit Card", }, }, - transactions: []handlers.Transaction{ + transactions: []models.Transaction{ { UserId: 0, Description: "weekly groceries", Date: time.Date(2017, time.October, 15, 1, 16, 59, 0, time.UTC), - Splits: []*handlers.Split{ + Splits: []*models.Split{ { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 1, SecurityId: -1, Amount: "-5.6", }, { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 3, SecurityId: -1, Amount: "5.6", @@ -328,15 +328,15 @@ var data = []TestData{ UserId: 0, Description: "weekly groceries", Date: time.Date(2017, time.October, 31, 19, 10, 14, 0, time.UTC), - Splits: []*handlers.Split{ + Splits: []*models.Split{ { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 1, SecurityId: -1, Amount: "-81.59", }, { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 3, SecurityId: -1, Amount: "81.59", @@ -347,15 +347,15 @@ var data = []TestData{ UserId: 0, Description: "Cable", Date: time.Date(2017, time.September, 2, 0, 00, 00, 0, time.UTC), - Splits: []*handlers.Split{ + Splits: []*models.Split{ { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 1, SecurityId: -1, Amount: "-39.99", }, { - Status: handlers.Entered, + Status: models.Entered, AccountId: 4, SecurityId: -1, Amount: "39.99", @@ -366,15 +366,15 @@ var data = []TestData{ UserId: 1, Description: "Gas", Date: time.Date(2017, time.November, 1, 13, 19, 50, 0, time.UTC), - Splits: []*handlers.Split{ + Splits: []*models.Split{ { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: 5, SecurityId: -1, Amount: "-24.56", }, { - Status: handlers.Entered, + Status: models.Entered, AccountId: 6, SecurityId: -1, Amount: "24.56", diff --git a/internal/handlers/transactions.go b/internal/handlers/transactions.go index 60c97b2..3795ce7 100644 --- a/internal/handlers/transactions.go +++ b/internal/handlers/transactions.go @@ -1,7 +1,6 @@ package handlers import ( - "encoding/json" "errors" "fmt" "github.com/aclindsa/moneygo/internal/models" @@ -10,141 +9,17 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" ) -// Split.Status -const ( - Imported int64 = 1 - Entered = 2 - Cleared = 3 - Reconciled = 4 - Voided = 5 -) - -// Split.ImportSplitType -const ( - Default int64 = 0 - ImportAccount = 1 // This split belongs to the main account being imported - SubAccount = 2 // This split belongs to a sub-account of that being imported - ExternalAccount = 3 - TradingAccount = 4 - Commission = 5 - Taxes = 6 - Fees = 7 - Load = 8 - IncomeAccount = 9 - ExpenseAccount = 10 -) - -type Split struct { - SplitId int64 - TransactionId int64 - Status int64 - ImportSplitType int64 - - // One of AccountId and SecurityId must be -1 - // In normal splits, AccountId will be valid and SecurityId will be -1. The - // only case where this is reversed is for transactions that have been - // imported and not yet associated with an account. - AccountId int64 - SecurityId int64 - - RemoteId string // unique ID from server, for detecting duplicates - Number string // Check or reference number - Memo string - Amount string // String representation of decimal, suitable for passing to big.Rat.SetString() -} - -func GetBigAmount(amt string) (*big.Rat, error) { - var r big.Rat - _, success := r.SetString(amt) - if !success { - return nil, errors.New("Couldn't convert string amount to big.Rat via SetString()") - } - return &r, nil -} - -func (s *Split) GetAmount() (*big.Rat, error) { - return GetBigAmount(s.Amount) -} - -func (s *Split) Valid() bool { - if (s.AccountId == -1) == (s.SecurityId == -1) { - return false - } - _, err := s.GetAmount() - return err == nil -} - -func (s *Split) AlreadyImported(tx *Tx) (bool, error) { +func SplitAlreadyImported(tx *Tx, s *models.Split) (bool, error) { count, err := tx.SelectInt("SELECT COUNT(*) from splits where RemoteId=? and AccountId=?", s.RemoteId, s.AccountId) return count == 1, err } -type Transaction struct { - TransactionId int64 - UserId int64 - Description string - Date time.Time - Splits []*Split `db:"-"` -} - -type TransactionList struct { - Transactions *[]Transaction `json:"transactions"` -} - -type AccountTransactionsList struct { - Account *Account - Transactions *[]Transaction - TotalTransactions int64 - BeginningBalance string - EndingBalance string -} - -func (t *Transaction) Write(w http.ResponseWriter) error { - enc := json.NewEncoder(w) - return enc.Encode(t) -} - -func (t *Transaction) Read(json_str string) error { - dec := json.NewDecoder(strings.NewReader(json_str)) - return dec.Decode(t) -} - -func (tl *TransactionList) Write(w http.ResponseWriter) error { - enc := json.NewEncoder(w) - return enc.Encode(tl) -} - -func (tl *TransactionList) Read(json_str string) error { - dec := json.NewDecoder(strings.NewReader(json_str)) - return dec.Decode(tl) -} - -func (atl *AccountTransactionsList) Write(w http.ResponseWriter) error { - enc := json.NewEncoder(w) - return enc.Encode(atl) -} - -func (atl *AccountTransactionsList) Read(json_str string) error { - dec := json.NewDecoder(strings.NewReader(json_str)) - return dec.Decode(atl) -} - -func (t *Transaction) Valid() bool { - for i := range t.Splits { - if !t.Splits[i].Valid() { - return false - } - } - return true -} - // Return a map of security ID's to big.Rat's containing the amount that // security is imbalanced by -func (t *Transaction) GetImbalances(tx *Tx) (map[int64]big.Rat, error) { +func GetTransactionImbalances(tx *Tx, t *models.Transaction) (map[int64]big.Rat, error) { sums := make(map[int64]big.Rat) if !t.Valid() { @@ -155,7 +30,7 @@ func (t *Transaction) GetImbalances(tx *Tx) (map[int64]big.Rat, error) { securityid := t.Splits[i].SecurityId if t.Splits[i].AccountId != -1 { var err error - var account *Account + var account *models.Account account, err = GetAccount(tx, t.Splits[i].AccountId, t.UserId) if err != nil { return nil, err @@ -172,10 +47,10 @@ func (t *Transaction) GetImbalances(tx *Tx) (map[int64]big.Rat, error) { // Returns true if all securities contained in this transaction are balanced, // false otherwise -func (t *Transaction) Balanced(tx *Tx) (bool, error) { +func TransactionBalanced(tx *Tx, t *models.Transaction) (bool, error) { var zero big.Rat - sums, err := t.GetImbalances(tx) + sums, err := GetTransactionImbalances(tx, t) if err != nil { return false, err } @@ -188,8 +63,8 @@ func (t *Transaction) Balanced(tx *Tx) (bool, error) { return true, nil } -func GetTransaction(tx *Tx, transactionid int64, userid int64) (*Transaction, error) { - var t Transaction +func GetTransaction(tx *Tx, transactionid int64, userid int64) (*models.Transaction, error) { + var t models.Transaction err := tx.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid) if err != nil { @@ -204,8 +79,8 @@ func GetTransaction(tx *Tx, transactionid int64, userid int64) (*Transaction, er return &t, nil } -func GetTransactions(tx *Tx, userid int64) (*[]Transaction, error) { - var transactions []Transaction +func GetTransactions(tx *Tx, userid int64) (*[]models.Transaction, error) { + var transactions []models.Transaction _, err := tx.Select(&transactions, "SELECT * from transactions where UserId=?", userid) if err != nil { @@ -246,7 +121,7 @@ func (ame AccountMissingError) Error() string { return "Account missing" } -func InsertTransaction(tx *Tx, t *Transaction, user *models.User) error { +func InsertTransaction(tx *Tx, t *models.Transaction, user *models.User) error { // Map of any accounts with transaction splits being added a_map := make(map[int64]bool) for i := range t.Splits { @@ -296,8 +171,8 @@ func InsertTransaction(tx *Tx, t *Transaction, user *models.User) error { return nil } -func UpdateTransaction(tx *Tx, t *Transaction, user *models.User) error { - var existing_splits []*Split +func UpdateTransaction(tx *Tx, t *models.Transaction, user *models.User) error { + var existing_splits []*models.Split _, err := tx.Select(&existing_splits, "SELECT * from splits where TransactionId=?", t.TransactionId) if err != nil { @@ -373,7 +248,7 @@ func UpdateTransaction(tx *Tx, t *Transaction, user *models.User) error { return nil } -func DeleteTransaction(tx *Tx, t *Transaction, user *models.User) error { +func DeleteTransaction(tx *Tx, t *models.Transaction, user *models.User) error { var accountids []int64 _, err := tx.Select(&accountids, "SELECT DISTINCT AccountId FROM splits WHERE TransactionId=? AND AccountId != -1", t.TransactionId) if err != nil { @@ -408,7 +283,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter } if r.Method == "POST" { - var transaction Transaction + var transaction models.Transaction if err := ReadJSON(r, &transaction); err != nil { return NewError(3 /*Invalid Request*/) } @@ -427,7 +302,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter } } - balanced, err := transaction.Balanced(context.Tx) + balanced, err := TransactionBalanced(context.Tx, &transaction) if err != nil { return NewError(999 /*Internal Error*/) } @@ -449,7 +324,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter } else if r.Method == "GET" { if context.LastLevel() { //Return all Transactions - var al TransactionList + var al models.TransactionList transactions, err := GetTransactions(context.Tx, user.UserId) if err != nil { log.Print(err) @@ -475,13 +350,13 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter return NewError(3 /*Invalid Request*/) } if r.Method == "PUT" { - var transaction Transaction + var transaction models.Transaction if err := ReadJSON(r, &transaction); err != nil || transaction.TransactionId != transactionid { return NewError(3 /*Invalid Request*/) } transaction.UserId = user.UserId - balanced, err := transaction.Balanced(context.Tx) + balanced, err := TransactionBalanced(context.Tx, &transaction) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) @@ -526,7 +401,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter return NewError(3 /*Invalid Request*/) } -func TransactionsBalanceDifference(tx *Tx, accountid int64, transactions []Transaction) (*big.Rat, error) { +func TransactionsBalanceDifference(tx *Tx, accountid int64, transactions []models.Transaction) (*big.Rat, error) { var pageDifference, tmp big.Rat for i := range transactions { _, err := tx.Select(&transactions[i].Splits, "SELECT * FROM splits where TransactionId=?", transactions[i].TransactionId) @@ -538,7 +413,7 @@ func TransactionsBalanceDifference(tx *Tx, accountid int64, transactions []Trans // an ending balance for j := range transactions[i].Splits { if transactions[i].Splits[j].AccountId == accountid { - rat_amount, err := GetBigAmount(transactions[i].Splits[j].Amount) + rat_amount, err := models.GetBigAmount(transactions[i].Splits[j].Amount) if err != nil { return nil, err } @@ -551,7 +426,7 @@ func TransactionsBalanceDifference(tx *Tx, accountid int64, transactions []Trans } func GetAccountBalance(tx *Tx, user *models.User, accountid int64) (*big.Rat, error) { - var splits []Split + var splits []models.Split sql := "SELECT DISTINCT splits.* FROM splits INNER JOIN transactions ON transactions.TransactionId = splits.TransactionId WHERE splits.AccountId=? AND transactions.UserId=?" _, err := tx.Select(&splits, sql, accountid, user.UserId) @@ -561,7 +436,7 @@ func GetAccountBalance(tx *Tx, user *models.User, accountid int64) (*big.Rat, er var balance, tmp big.Rat for _, s := range splits { - rat_amount, err := GetBigAmount(s.Amount) + rat_amount, err := models.GetBigAmount(s.Amount) if err != nil { return nil, err } @@ -574,7 +449,7 @@ func GetAccountBalance(tx *Tx, user *models.User, accountid int64) (*big.Rat, er // Assumes accountid is valid and is owned by the current user func GetAccountBalanceDate(tx *Tx, user *models.User, accountid int64, date *time.Time) (*big.Rat, error) { - var splits []Split + var splits []models.Split sql := "SELECT DISTINCT splits.* FROM splits INNER JOIN transactions ON transactions.TransactionId = splits.TransactionId WHERE splits.AccountId=? AND transactions.UserId=? AND transactions.Date < ?" _, err := tx.Select(&splits, sql, accountid, user.UserId, date) @@ -584,7 +459,7 @@ func GetAccountBalanceDate(tx *Tx, user *models.User, accountid int64, date *tim var balance, tmp big.Rat for _, s := range splits { - rat_amount, err := GetBigAmount(s.Amount) + rat_amount, err := models.GetBigAmount(s.Amount) if err != nil { return nil, err } @@ -596,7 +471,7 @@ func GetAccountBalanceDate(tx *Tx, user *models.User, accountid int64, date *tim } func GetAccountBalanceDateRange(tx *Tx, user *models.User, accountid int64, begin, end *time.Time) (*big.Rat, error) { - var splits []Split + var splits []models.Split sql := "SELECT DISTINCT splits.* FROM splits INNER JOIN transactions ON transactions.TransactionId = splits.TransactionId WHERE splits.AccountId=? AND transactions.UserId=? AND transactions.Date >= ? AND transactions.Date < ?" _, err := tx.Select(&splits, sql, accountid, user.UserId, begin, end) @@ -606,7 +481,7 @@ func GetAccountBalanceDateRange(tx *Tx, user *models.User, accountid int64, begi var balance, tmp big.Rat for _, s := range splits { - rat_amount, err := GetBigAmount(s.Amount) + rat_amount, err := models.GetBigAmount(s.Amount) if err != nil { return nil, err } @@ -617,9 +492,9 @@ func GetAccountBalanceDateRange(tx *Tx, user *models.User, accountid int64, begi return &balance, nil } -func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort string, page uint64, limit uint64) (*AccountTransactionsList, error) { - var transactions []Transaction - var atl AccountTransactionsList +func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort string, page uint64, limit uint64) (*models.AccountTransactionsList, error) { + var transactions []models.Transaction + var atl models.AccountTransactionsList var sqlsort, balanceLimitOffset string var balanceLimitOffsetArg uint64 @@ -685,7 +560,7 @@ func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort str var tmp, balance big.Rat for _, amount := range amounts { - rat_amount, err := GetBigAmount(amount) + rat_amount, err := models.GetBigAmount(amount) if err != nil { return nil, err } diff --git a/internal/handlers/transactions_test.go b/internal/handlers/transactions_test.go index 7d6285f..0f3b68d 100644 --- a/internal/handlers/transactions_test.go +++ b/internal/handlers/transactions_test.go @@ -3,6 +3,7 @@ package handlers_test import ( "fmt" "github.com/aclindsa/moneygo/internal/handlers" + "github.com/aclindsa/moneygo/internal/models" "net/http" "net/url" "strconv" @@ -10,14 +11,14 @@ import ( "time" ) -func createTransaction(client *http.Client, transaction *handlers.Transaction) (*handlers.Transaction, error) { - var s handlers.Transaction +func createTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) { + var s models.Transaction err := create(client, transaction, &s, "/v1/transactions/") return &s, err } -func getTransaction(client *http.Client, transactionid int64) (*handlers.Transaction, error) { - var s handlers.Transaction +func getTransaction(client *http.Client, transactionid int64) (*models.Transaction, error) { + var s models.Transaction err := read(client, &s, "/v1/transactions/"+strconv.FormatInt(transactionid, 10)) if err != nil { return nil, err @@ -25,8 +26,8 @@ func getTransaction(client *http.Client, transactionid int64) (*handlers.Transac return &s, nil } -func getTransactions(client *http.Client) (*handlers.TransactionList, error) { - var tl handlers.TransactionList +func getTransactions(client *http.Client) (*models.TransactionList, error) { + var tl models.TransactionList err := read(client, &tl, "/v1/transactions/") if err != nil { return nil, err @@ -34,8 +35,8 @@ func getTransactions(client *http.Client) (*handlers.TransactionList, error) { return &tl, nil } -func getAccountTransactions(client *http.Client, accountid, page, limit int64, sort string) (*handlers.AccountTransactionsList, error) { - var atl handlers.AccountTransactionsList +func getAccountTransactions(client *http.Client, accountid, page, limit int64, sort string) (*models.AccountTransactionsList, error) { + var atl models.AccountTransactionsList params := url.Values{} query := fmt.Sprintf("/v1/accounts/%d/transactions/", accountid) @@ -57,8 +58,8 @@ func getAccountTransactions(client *http.Client, accountid, page, limit int64, s return &atl, nil } -func updateTransaction(client *http.Client, transaction *handlers.Transaction) (*handlers.Transaction, error) { - var s handlers.Transaction +func updateTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) { + var s models.Transaction err := update(client, transaction, &s, "/v1/transactions/"+strconv.FormatInt(transaction.TransactionId, 10)) if err != nil { return nil, err @@ -66,7 +67,7 @@ func updateTransaction(client *http.Client, transaction *handlers.Transaction) ( return &s, nil } -func deleteTransaction(client *http.Client, s *handlers.Transaction) error { +func deleteTransaction(client *http.Client, s *models.Transaction) error { err := remove(client, "/v1/transactions/"+strconv.FormatInt(s.TransactionId, 10)) if err != nil { return err @@ -74,7 +75,7 @@ func deleteTransaction(client *http.Client, s *handlers.Transaction) error { return nil } -func ensureTransactionsMatch(t *testing.T, expected, tran *handlers.Transaction, accounts *[]handlers.Account, matchtransactionids, matchsplitids bool) { +func ensureTransactionsMatch(t *testing.T, expected, tran *models.Transaction, accounts *[]models.Account, matchtransactionids, matchsplitids bool) { t.Helper() if tran.TransactionId == 0 { @@ -136,9 +137,9 @@ func ensureTransactionsMatch(t *testing.T, expected, tran *handlers.Transaction, } } -func getAccountVersionMap(t *testing.T, client *http.Client, tran *handlers.Transaction) map[int64]*handlers.Account { +func getAccountVersionMap(t *testing.T, client *http.Client, tran *models.Transaction) map[int64]*models.Account { t.Helper() - accountMap := make(map[int64]*handlers.Account) + accountMap := make(map[int64]*models.Account) for _, split := range tran.Splits { account, err := getAccount(client, split.AccountId) if err != nil { @@ -149,7 +150,7 @@ func getAccountVersionMap(t *testing.T, client *http.Client, tran *handlers.Tran return accountMap } -func checkAccountVersionsUpdated(t *testing.T, client *http.Client, accountMap map[int64]*handlers.Account, tran *handlers.Transaction) { +func checkAccountVersionsUpdated(t *testing.T, client *http.Client, accountMap map[int64]*models.Account, tran *models.Transaction) { for _, split := range tran.Splits { account, err := getAccount(client, split.AccountId) if err != nil { @@ -177,19 +178,19 @@ func TestCreateTransaction(t *testing.T) { } // Don't allow imbalanced transactions - tran := handlers.Transaction{ + tran := models.Transaction{ UserId: d.users[0].UserId, Description: "Imbalanced", Date: time.Date(2017, time.September, 1, 0, 00, 00, 0, time.UTC), - Splits: []*handlers.Split{ + Splits: []*models.Split{ { - Status: handlers.Reconciled, + Status: models.Reconciled, AccountId: d.accounts[1].AccountId, SecurityId: -1, Amount: "-39.98", }, { - Status: handlers.Entered, + Status: models.Entered, AccountId: d.accounts[4].AccountId, SecurityId: -1, Amount: "39.99", @@ -209,7 +210,7 @@ func TestCreateTransaction(t *testing.T) { } // Don't allow transactions with 0 splits - tran.Splits = []*handlers.Split{} + tran.Splits = []*models.Split{} _, err = createTransaction(d.clients[0], &tran) if err == nil { t.Fatalf("Expected error creating with zero splits") @@ -316,9 +317,9 @@ func TestUpdateTransaction(t *testing.T) { ensureTransactionsMatch(t, &curr, tran, nil, true, true) - tran.Splits = []*handlers.Split{} + tran.Splits = []*models.Split{} for _, s := range curr.Splits { - var split handlers.Split + var split models.Split split = *s tran.Splits = append(tran.Splits, &split) } @@ -346,7 +347,7 @@ func TestUpdateTransaction(t *testing.T) { } // Don't allow transactions with 0 splits - tran.Splits = []*handlers.Split{} + tran.Splits = []*models.Split{} _, err = updateTransaction(d.clients[orig.UserId], tran) if err == nil { t.Fatalf("Expected error updating with zero splits") @@ -391,12 +392,12 @@ func TestDeleteTransaction(t *testing.T) { }) } -func helperTestAccountTransactions(t *testing.T, d *TestData, account *handlers.Account, limit int64, sort string) { +func helperTestAccountTransactions(t *testing.T, d *TestData, account *models.Account, limit int64, sort string) { if account.UserId != d.users[0].UserId { return } - var transactions []handlers.Transaction + var transactions []models.Transaction var lastFetchCount int64 for page := int64(0); page == 0 || lastFetchCount > 0; page++ { diff --git a/internal/models/accounts.go b/internal/models/accounts.go new file mode 100644 index 0000000..fdfac98 --- /dev/null +++ b/internal/models/accounts.go @@ -0,0 +1,118 @@ +package models + +import ( + "encoding/json" + "net/http" + "strings" +) + +type AccountType int64 + +const ( + Bank AccountType = 1 // start at 1 so that the default (0) is invalid + Cash = 2 + Asset = 3 + Liability = 4 + Investment = 5 + Income = 6 + Expense = 7 + Trading = 8 + Equity = 9 + Receivable = 10 + Payable = 11 +) + +var AccountTypes = []AccountType{ + Bank, + Cash, + Asset, + Liability, + Investment, + Income, + Expense, + Trading, + Equity, + Receivable, + Payable, +} + +func (t AccountType) String() string { + switch t { + case Bank: + return "Bank" + case Cash: + return "Cash" + case Asset: + return "Asset" + case Liability: + return "Liability" + case Investment: + return "Investment" + case Income: + return "Income" + case Expense: + return "Expense" + case Trading: + return "Trading" + case Equity: + return "Equity" + case Receivable: + return "Receivable" + case Payable: + return "Payable" + } + return "" +} + +type Account struct { + AccountId int64 + ExternalAccountId string + UserId int64 + SecurityId int64 + ParentAccountId int64 // -1 if this account is at the root + Type AccountType + Name string + + // monotonically-increasing account transaction version number. Used for + // allowing a client to ensure they have a consistent version when paging + // through transactions. + AccountVersion int64 `json:"Version"` + + // Optional fields specifying how to fetch transactions from a bank via OFX + OFXURL string + OFXORG string + OFXFID string + OFXUser string + OFXBankID string // OFX BankID (BrokerID if AcctType == Investment) + OFXAcctID string + OFXAcctType string // ofxgo.acctType + OFXClientUID string + OFXAppID string + OFXAppVer string + OFXVersion string + OFXNoIndent bool +} + +type AccountList struct { + Accounts *[]Account `json:"accounts"` +} + +func (a *Account) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(a) +} + +func (a *Account) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(a) +} + +func (al *AccountList) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(al) +} + +func (al *AccountList) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(al) +} diff --git a/internal/models/transactions.go b/internal/models/transactions.go new file mode 100644 index 0000000..8076995 --- /dev/null +++ b/internal/models/transactions.go @@ -0,0 +1,133 @@ +package models + +import ( + "encoding/json" + "errors" + "math/big" + "net/http" + "strings" + "time" +) + +// Split.Status +const ( + Imported int64 = 1 + Entered = 2 + Cleared = 3 + Reconciled = 4 + Voided = 5 +) + +// Split.ImportSplitType +const ( + Default int64 = 0 + ImportAccount = 1 // This split belongs to the main account being imported + SubAccount = 2 // This split belongs to a sub-account of that being imported + ExternalAccount = 3 + TradingAccount = 4 + Commission = 5 + Taxes = 6 + Fees = 7 + Load = 8 + IncomeAccount = 9 + ExpenseAccount = 10 +) + +type Split struct { + SplitId int64 + TransactionId int64 + Status int64 + ImportSplitType int64 + + // One of AccountId and SecurityId must be -1 + // In normal splits, AccountId will be valid and SecurityId will be -1. The + // only case where this is reversed is for transactions that have been + // imported and not yet associated with an account. + AccountId int64 + SecurityId int64 + + RemoteId string // unique ID from server, for detecting duplicates + Number string // Check or reference number + Memo string + Amount string // String representation of decimal, suitable for passing to big.Rat.SetString() +} + +func GetBigAmount(amt string) (*big.Rat, error) { + var r big.Rat + _, success := r.SetString(amt) + if !success { + return nil, errors.New("Couldn't convert string amount to big.Rat via SetString()") + } + return &r, nil +} + +func (s *Split) GetAmount() (*big.Rat, error) { + return GetBigAmount(s.Amount) +} + +func (s *Split) Valid() bool { + if (s.AccountId == -1) == (s.SecurityId == -1) { + return false + } + _, err := s.GetAmount() + return err == nil +} + +type Transaction struct { + TransactionId int64 + UserId int64 + Description string + Date time.Time + Splits []*Split `db:"-"` +} + +type TransactionList struct { + Transactions *[]Transaction `json:"transactions"` +} + +type AccountTransactionsList struct { + Account *Account + Transactions *[]Transaction + TotalTransactions int64 + BeginningBalance string + EndingBalance string +} + +func (t *Transaction) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(t) +} + +func (t *Transaction) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(t) +} + +func (tl *TransactionList) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(tl) +} + +func (tl *TransactionList) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(tl) +} + +func (atl *AccountTransactionsList) Write(w http.ResponseWriter) error { + enc := json.NewEncoder(w) + return enc.Encode(atl) +} + +func (atl *AccountTransactionsList) Read(json_str string) error { + dec := json.NewDecoder(strings.NewReader(json_str)) + return dec.Decode(atl) +} + +func (t *Transaction) Valid() bool { + for i := range t.Splits { + if !t.Splits[i].Valid() { + return false + } + } + return true +}