Split accounts and transactions into models

This commit is contained in:
Aaron Lindsay 2017-12-04 05:55:25 -05:00
parent f72c86ef58
commit 128ea57c4d
16 changed files with 568 additions and 553 deletions

View File

@ -36,10 +36,10 @@ func GetDbMap(db *sql.DB, dbtype config.DbType) (*gorp.DbMap, error) {
dbmap := &gorp.DbMap{Db: db, Dialect: dialect} dbmap := &gorp.DbMap{Db: db, Dialect: dialect}
dbmap.AddTableWithName(models.User{}, "users").SetKeys(true, "UserId") dbmap.AddTableWithName(models.User{}, "users").SetKeys(true, "UserId")
dbmap.AddTableWithName(models.Session{}, "sessions").SetKeys(true, "SessionId") 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(models.Security{}, "securities").SetKeys(true, "SecurityId")
dbmap.AddTableWithName(handlers.Transaction{}, "transactions").SetKeys(true, "TransactionId") dbmap.AddTableWithName(models.Transaction{}, "transactions").SetKeys(true, "TransactionId")
dbmap.AddTableWithName(handlers.Split{}, "splits").SetKeys(true, "SplitId") dbmap.AddTableWithName(models.Split{}, "splits").SetKeys(true, "SplitId")
dbmap.AddTableWithName(handlers.Price{}, "prices").SetKeys(true, "PriceId") dbmap.AddTableWithName(handlers.Price{}, "prices").SetKeys(true, "PriceId")
rtable := dbmap.AddTableWithName(handlers.Report{}, "reports").SetKeys(true, "ReportId") rtable := dbmap.AddTableWithName(handlers.Report{}, "reports").SetKeys(true, "ReportId")
rtable.ColMap("Lua").SetMaxSize(handlers.LuaMaxLength + luaMaxLengthBuffer) rtable.ColMap("Lua").SetMaxSize(handlers.LuaMaxLength + luaMaxLengthBuffer)

View File

@ -1,127 +1,14 @@
package handlers package handlers
import ( import (
"encoding/json"
"errors" "errors"
"github.com/aclindsa/moneygo/internal/models" "github.com/aclindsa/moneygo/internal/models"
"log" "log"
"net/http" "net/http"
"strings"
) )
type AccountType int64 func GetAccount(tx *Tx, accountid int64, userid int64) (*models.Account, error) {
var a models.Account
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
err := tx.SelectOne(&a, "SELECT * from accounts where UserId=? AND AccountId=?", userid, accountid) err := tx.SelectOne(&a, "SELECT * from accounts where UserId=? AND AccountId=?", userid, accountid)
if err != nil { if err != nil {
@ -130,8 +17,8 @@ func GetAccount(tx *Tx, accountid int64, userid int64) (*Account, error) {
return &a, nil return &a, nil
} }
func GetAccounts(tx *Tx, userid int64) (*[]Account, error) { func GetAccounts(tx *Tx, userid int64) (*[]models.Account, error) {
var accounts []Account var accounts []models.Account
_, err := tx.Select(&accounts, "SELECT * from accounts where UserId=?", userid) _, err := tx.Select(&accounts, "SELECT * from accounts where UserId=?", userid)
if err != nil { 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, // Get (and attempt to create if it doesn't exist). Matches on UserId,
// SecurityId, Type, Name, and ParentAccountId // SecurityId, Type, Name, and ParentAccountId
func GetCreateAccount(tx *Tx, a Account) (*Account, error) { func GetCreateAccount(tx *Tx, a models.Account) (*models.Account, error) {
var accounts []Account var accounts []models.Account
var account Account var account models.Account
// Try to find the top-level trading 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) _, 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 // Get (and attempt to create if it doesn't exist) the security/currency
// trading account for the supplied security/currency // trading account for the supplied security/currency
func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error) { func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*models.Account, error) {
var tradingAccount Account var tradingAccount models.Account
var account Account var account models.Account
user, err := GetUser(tx, userid) user, err := GetUser(tx, userid)
if err != nil { if err != nil {
@ -180,7 +67,7 @@ func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error)
} }
tradingAccount.UserId = userid tradingAccount.UserId = userid
tradingAccount.Type = Trading tradingAccount.Type = models.Trading
tradingAccount.Name = "Trading" tradingAccount.Name = "Trading"
tradingAccount.SecurityId = user.DefaultCurrency tradingAccount.SecurityId = user.DefaultCurrency
tradingAccount.ParentAccountId = -1 tradingAccount.ParentAccountId = -1
@ -200,7 +87,7 @@ func GetTradingAccount(tx *Tx, userid int64, securityid int64) (*Account, error)
account.Name = security.Name account.Name = security.Name
account.ParentAccountId = ta.AccountId account.ParentAccountId = ta.AccountId
account.SecurityId = securityid account.SecurityId = securityid
account.Type = Trading account.Type = models.Trading
a, err := GetCreateAccount(tx, account) a, err := GetCreateAccount(tx, account)
if err != nil { 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 // Get (and attempt to create if it doesn't exist) the security/currency
// imbalance account for the supplied security/currency // imbalance account for the supplied security/currency
func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*Account, error) { func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*models.Account, error) {
var imbalanceAccount Account var imbalanceAccount models.Account
var account Account var account models.Account
xxxtemplate := FindSecurityTemplate("XXX", models.Currency) xxxtemplate := FindSecurityTemplate("XXX", models.Currency)
if xxxtemplate == nil { if xxxtemplate == nil {
return nil, errors.New("Couldn't find XXX security template") 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.Name = "Imbalances"
imbalanceAccount.ParentAccountId = -1 imbalanceAccount.ParentAccountId = -1
imbalanceAccount.SecurityId = xxxsecurity.SecurityId imbalanceAccount.SecurityId = xxxsecurity.SecurityId
imbalanceAccount.Type = Bank imbalanceAccount.Type = models.Bank
// Find/create the top-level trading account // Find/create the top-level trading account
ia, err := GetCreateAccount(tx, imbalanceAccount) ia, err := GetCreateAccount(tx, imbalanceAccount)
@ -245,7 +132,7 @@ func GetImbalanceAccount(tx *Tx, userid int64, securityid int64) (*Account, erro
account.Name = security.Name account.Name = security.Name
account.ParentAccountId = ia.AccountId account.ParentAccountId = ia.AccountId
account.SecurityId = securityid account.SecurityId = securityid
account.Type = Bank account.Type = models.Bank
a, err := GetCreateAccount(tx, account) a, err := GetCreateAccount(tx, account)
if err != nil { if err != nil {
@ -273,7 +160,7 @@ func (cae CircularAccountsError) Error() string {
return "Would result in circular account relationship" 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) found := make(map[int64]bool)
if !insert { if !insert {
found[a.AccountId] = true found[a.AccountId] = true
@ -286,7 +173,7 @@ func insertUpdateAccount(tx *Tx, a *Account, insert bool) error {
return TooMuchNestingError{} return TooMuchNestingError{}
} }
var a Account var a models.Account
err := tx.SelectOne(&a, "SELECT * from accounts where AccountId=?", parentid) err := tx.SelectOne(&a, "SELECT * from accounts where AccountId=?", parentid)
if err != nil { if err != nil {
return ParentAccountMissingError{} return ParentAccountMissingError{}
@ -329,15 +216,15 @@ func insertUpdateAccount(tx *Tx, a *Account, insert bool) error {
return nil return nil
} }
func InsertAccount(tx *Tx, a *Account) error { func InsertAccount(tx *Tx, a *models.Account) error {
return insertUpdateAccount(tx, a, true) 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) return insertUpdateAccount(tx, a, false)
} }
func DeleteAccount(tx *Tx, a *Account) error { func DeleteAccount(tx *Tx, a *models.Account) error {
if a.ParentAccountId != -1 { if a.ParentAccountId != -1 {
// Re-parent splits to this account's parent account if this account isn't a root account // 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) _, 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) return AccountImportHandler(context, r, user, accountid)
} }
var account Account var account models.Account
if err := ReadJSON(r, &account); err != nil { if err := ReadJSON(r, &account); err != nil {
return NewError(3 /*Invalid Request*/) return NewError(3 /*Invalid Request*/)
} }
@ -415,7 +302,7 @@ func AccountHandler(r *http.Request, context *Context) ResponseWriterWriter {
} else if r.Method == "GET" { } else if r.Method == "GET" {
if context.LastLevel() { if context.LastLevel() {
//Return all Accounts //Return all Accounts
var al AccountList var al models.AccountList
accounts, err := GetAccounts(context.Tx, user.UserId) accounts, err := GetAccounts(context.Tx, user.UserId)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -447,7 +334,7 @@ func AccountHandler(r *http.Request, context *Context) ResponseWriterWriter {
return NewError(3 /*Invalid Request*/) return NewError(3 /*Invalid Request*/)
} }
if r.Method == "PUT" { if r.Method == "PUT" {
var account Account var account models.Account
if err := ReadJSON(r, &account); err != nil || account.AccountId != accountid { if err := ReadJSON(r, &account); err != nil || account.AccountId != accountid {
return NewError(3 /*Invalid Request*/) return NewError(3 /*Invalid Request*/)
} }

View File

@ -11,8 +11,8 @@ import (
const luaAccountTypeName = "account" const luaAccountTypeName = "account"
func luaContextGetAccounts(L *lua.LState) (map[int64]*Account, error) { func luaContextGetAccounts(L *lua.LState) (map[int64]*models.Account, error) {
var account_map map[int64]*Account var account_map map[int64]*models.Account
ctx := L.Context() 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") 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 { if !ok {
user, ok := ctx.Value(userContextKey).(*models.User) user, ok := ctx.Value(userContextKey).(*models.User)
if !ok { if !ok {
@ -33,7 +33,7 @@ func luaContextGetAccounts(L *lua.LState) (map[int64]*Account, error) {
return nil, err return nil, err
} }
account_map = make(map[int64]*Account) account_map = make(map[int64]*models.Account)
for i := range *accounts { for i := range *accounts {
account_map[(*accounts)[i].AccountId] = &(*accounts)[i] 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, "__eq", L.NewFunction(luaAccount__eq))
L.SetField(mt, "__metatable", lua.LString("protected")) 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))) L.SetField(mt, accttype.String(), lua.LNumber(float64(accttype)))
} }
@ -79,7 +79,7 @@ func luaRegisterAccounts(L *lua.LState) {
L.SetGlobal("get_accounts", getAccountsFn) 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 := L.NewUserData()
ud.Value = account ud.Value = account
L.SetMetatable(ud, L.GetTypeMetatable(luaAccountTypeName)) 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. // 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) ud := L.CheckUserData(n)
if account, ok := ud.Value.(*Account); ok { if account, ok := ud.Value.(*models.Account); ok {
return account return account
} }
L.ArgError(n, "account expected") L.ArgError(n, "account expected")

View File

@ -2,19 +2,20 @@ package handlers_test
import ( import (
"github.com/aclindsa/moneygo/internal/handlers" "github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http" "net/http"
"strconv" "strconv"
"testing" "testing"
) )
func createAccount(client *http.Client, account *handlers.Account) (*handlers.Account, error) { func createAccount(client *http.Client, account *models.Account) (*models.Account, error) {
var a handlers.Account var a models.Account
err := create(client, account, &a, "/v1/accounts/") err := create(client, account, &a, "/v1/accounts/")
return &a, err return &a, err
} }
func getAccount(client *http.Client, accountid int64) (*handlers.Account, error) { func getAccount(client *http.Client, accountid int64) (*models.Account, error) {
var a handlers.Account var a models.Account
err := read(client, &a, "/v1/accounts/"+strconv.FormatInt(accountid, 10)) err := read(client, &a, "/v1/accounts/"+strconv.FormatInt(accountid, 10))
if err != nil { if err != nil {
return nil, err return nil, err
@ -22,8 +23,8 @@ func getAccount(client *http.Client, accountid int64) (*handlers.Account, error)
return &a, nil return &a, nil
} }
func getAccounts(client *http.Client) (*handlers.AccountList, error) { func getAccounts(client *http.Client) (*models.AccountList, error) {
var al handlers.AccountList var al models.AccountList
err := read(client, &al, "/v1/accounts/") err := read(client, &al, "/v1/accounts/")
if err != nil { if err != nil {
return nil, err return nil, err
@ -31,8 +32,8 @@ func getAccounts(client *http.Client) (*handlers.AccountList, error) {
return &al, nil return &al, nil
} }
func updateAccount(client *http.Client, account *handlers.Account) (*handlers.Account, error) { func updateAccount(client *http.Client, account *models.Account) (*models.Account, error) {
var a handlers.Account var a models.Account
err := update(client, account, &a, "/v1/accounts/"+strconv.FormatInt(account.AccountId, 10)) err := update(client, account, &a, "/v1/accounts/"+strconv.FormatInt(account.AccountId, 10))
if err != nil { if err != nil {
return nil, err return nil, err
@ -40,7 +41,7 @@ func updateAccount(client *http.Client, account *handlers.Account) (*handlers.Ac
return &a, nil 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)) err := remove(client, "/v1/accounts/"+strconv.FormatInt(a.AccountId, 10))
if err != nil { if err != nil {
return err return err
@ -137,7 +138,7 @@ func TestUpdateAccount(t *testing.T) {
curr := d.accounts[i] curr := d.accounts[i]
curr.Name = "blah" curr.Name = "blah"
curr.Type = handlers.Payable curr.Type = models.Payable
for _, s := range d.securities { for _, s := range d.securities {
if s.UserId == curr.UserId { if s.UserId == curr.UserId {
curr.SecurityId = s.SecurityId curr.SecurityId = s.SecurityId

View File

@ -7,6 +7,7 @@ import (
"github.com/aclindsa/moneygo/internal/config" "github.com/aclindsa/moneygo/internal/config"
"github.com/aclindsa/moneygo/internal/db" "github.com/aclindsa/moneygo/internal/db"
"github.com/aclindsa/moneygo/internal/handlers" "github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
@ -202,7 +203,7 @@ func uploadFile(client *http.Client, filename, urlsuffix string) error {
return nil 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() t.Helper()
transactions, err := getAccountTransactions(client, account.AccountId, 0, 0, "") transactions, err := getAccountTransactions(client, account.AccountId, 0, 0, "")
if err != nil { if err != nil {

View File

@ -127,8 +127,8 @@ type GnucashXMLImport struct {
type GnucashImport struct { type GnucashImport struct {
Securities []models.Security Securities []models.Security
Accounts []Account Accounts []models.Account
Transactions []Transaction Transactions []models.Transaction
Prices []Price Prices []Price
} }
@ -206,7 +206,7 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) {
//Translate to our account format, figuring out parent relationships //Translate to our account format, figuring out parent relationships
for guid := range accountMap { for guid := range accountMap {
ga := accountMap[guid] ga := accountMap[guid]
var a Account var a models.Account
a.AccountId = ga.accountid a.AccountId = ga.accountid
if ga.ParentAccountId == rootAccount.AccountId { if ga.ParentAccountId == rootAccount.AccountId {
@ -229,29 +229,29 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) {
//TODO find account types //TODO find account types
switch ga.Type { switch ga.Type {
default: default:
a.Type = Bank a.Type = models.Bank
case "ASSET": case "ASSET":
a.Type = Asset a.Type = models.Asset
case "BANK": case "BANK":
a.Type = Bank a.Type = models.Bank
case "CASH": case "CASH":
a.Type = Cash a.Type = models.Cash
case "CREDIT", "LIABILITY": case "CREDIT", "LIABILITY":
a.Type = Liability a.Type = models.Liability
case "EQUITY": case "EQUITY":
a.Type = Equity a.Type = models.Equity
case "EXPENSE": case "EXPENSE":
a.Type = Expense a.Type = models.Expense
case "INCOME": case "INCOME":
a.Type = Income a.Type = models.Income
case "PAYABLE": case "PAYABLE":
a.Type = Payable a.Type = models.Payable
case "RECEIVABLE": case "RECEIVABLE":
a.Type = Receivable a.Type = models.Receivable
case "MUTUAL", "STOCK": case "MUTUAL", "STOCK":
a.Type = Investment a.Type = models.Investment
case "TRADING": case "TRADING":
a.Type = Trading a.Type = models.Trading
} }
gncimport.Accounts = append(gncimport.Accounts, a) gncimport.Accounts = append(gncimport.Accounts, a)
@ -261,20 +261,20 @@ func ImportGnucash(r io.Reader) (*GnucashImport, error) {
for i := range gncxml.Transactions { for i := range gncxml.Transactions {
gt := gncxml.Transactions[i] gt := gncxml.Transactions[i]
t := new(Transaction) t := new(models.Transaction)
t.Description = gt.Description t.Description = gt.Description
t.Date = gt.DatePosted.Date.Time t.Date = gt.DatePosted.Date.Time
for j := range gt.Splits { for j := range gt.Splits {
gs := gt.Splits[j] gs := gt.Splits[j]
s := new(Split) s := new(models.Split)
switch gs.Status { switch gs.Status {
default: // 'n', or not present default: // 'n', or not present
s.Status = Imported s.Status = models.Imported
case "c": case "c":
s.Status = Cleared s.Status = models.Cleared
case "y": case "y":
s.Status = Reconciled s.Status = models.Reconciled
} }
account, ok := accountMap[gs.AccountId] account, ok := accountMap[gs.AccountId]
@ -437,7 +437,7 @@ func GnucashImportHandler(r *http.Request, context *Context) ResponseWriterWrite
} }
split.AccountId = acctId split.AccountId = acctId
exists, err := split.AlreadyImported(context.Tx) exists, err := SplitAlreadyImported(context.Tx, split)
if err != nil { if err != nil {
log.Print("Error checking if split was already imported:", err) log.Print("Error checking if split was already imported:", err)
return NewError(999 /*Internal Error*/) return NewError(999 /*Internal Error*/)

View File

@ -1,7 +1,6 @@
package handlers_test package handlers_test
import ( import (
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models" "github.com/aclindsa/moneygo/internal/models"
"net/http" "net/http"
"testing" "testing"
@ -32,19 +31,19 @@ func TestImportGnucash(t *testing.T) {
} }
// Next, find the Expenses/Groceries account and verify it's balance // 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]) accounts, err := getAccounts(d.clients[0])
if err != nil { if err != nil {
t.Fatalf("Error fetching accounts: %s\n", err) t.Fatalf("Error fetching accounts: %s\n", err)
} }
for i, account := range *accounts.Accounts { 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] 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] 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] 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] expenses = &(*accounts.Accounts)[i]
} }
} }
@ -61,15 +60,15 @@ func TestImportGnucash(t *testing.T) {
t.Fatalf("Couldn't find 'Expenses' account") t.Fatalf("Couldn't find 'Expenses' account")
} }
for i, account := range *accounts.Accounts { 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] 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] 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] 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] 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] cable = &(*accounts.Accounts)[i]
} }
} }

View File

@ -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 // TODO Ensure all transactions have at least one split in the account
// we're importing to? // we're importing to?
var transactions []Transaction var transactions []models.Transaction
for _, transaction := range itl.Transactions { for _, transaction := range itl.Transactions {
transaction.UserId = user.UserId 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 // and fixup the SecurityId to be a valid one for this user's actual
// securities instead of a placeholder from the import // securities instead of a placeholder from the import
for _, split := range transaction.Splits { for _, split := range transaction.Splits {
split.Status = Imported split.Status = models.Imported
if split.AccountId != -1 { if split.AccountId != -1 {
if split.AccountId != importedAccount.AccountId { if split.AccountId != importedAccount.AccountId {
log.Print("Imported split's AccountId wasn't -1 but also didn't match the account") 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 { } else if split.SecurityId != -1 {
if sec, ok := securitymap[split.SecurityId]; ok { 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 // 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 // Find/make trading account if we're that type of split
trading_account, err := GetTradingAccount(tx, user.UserId, sec.SecurityId) trading_account, err := GetTradingAccount(tx, user.UserId, sec.SecurityId)
if err != nil { 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.AccountId = trading_account.AccountId
split.SecurityId = -1 split.SecurityId = -1
} else if split.ImportSplitType == SubAccount { } else if split.ImportSplitType == models.SubAccount {
subaccount := &Account{ subaccount := &models.Account{
UserId: user.UserId, UserId: user.UserId,
Name: sec.Name, Name: sec.Name,
ParentAccountId: account.AccountId, 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 { if err != nil {
log.Print(err) log.Print(err)
return NewError(999 /*Internal Error*/) 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 // Add new split to fixup imbalance
split := new(Split) split := new(models.Split)
r := new(big.Rat) r := new(big.Rat)
r.Neg(&imbalance) r.Neg(&imbalance)
security, err := GetSecurity(tx, imbalanced_security, user.UserId) 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 split.SecurityId = -1
} }
exists, err := split.AlreadyImported(tx) exists, err := SplitAlreadyImported(tx, split)
if err != nil { if err != nil {
log.Print("Error checking if split was already imported:", err) log.Print("Error checking if split was already imported:", err)
return NewError(999 /*Internal Error*/) 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*/) return NewError(999 /*Internal Error*/)
} }
if account.Type == Investment { if account.Type == models.Investment {
// Investment account // Investment account
statementRequest := ofxgo.InvStatementRequest{ statementRequest := ofxgo.InvStatementRequest{
TrnUID: *transactionuid, TrnUID: *transactionuid,

View File

@ -11,8 +11,8 @@ import (
type OFXImport struct { type OFXImport struct {
Securities []models.Security Securities []models.Security
Accounts []Account Accounts []models.Account
Transactions []Transaction Transactions []models.Transaction
// Balances map[int64]string // map AccountIDs to ending balances // 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 return &security, nil
} }
func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *Account) error { func (i *OFXImport) AddTransaction(tran *ofxgo.Transaction, account *models.Account) error {
var t Transaction var t models.Transaction
t.Date = tran.DtPosted.UTC() 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 { if len(tran.ExtdName) > 0 {
s1.Memo = tran.ExtdName.String() 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() s1.RemoteId = "ofx:" + tran.FiTID.String()
// TODO CorrectFiTID/CorrectAction? // TODO CorrectFiTID/CorrectAction?
s1.ImportSplitType = ImportAccount s1.ImportSplitType = models.ImportAccount
s2.ImportSplitType = ExternalAccount s2.ImportSplitType = models.ExternalAccount
security := i.Securities[account.SecurityId-1] security := i.Securities[account.SecurityId-1]
s1.Amount = amt.FloatString(security.Precision) s1.Amount = amt.FloatString(security.Precision)
s2.Amount = amt.Neg(amt).FloatString(security.Precision) s2.Amount = amt.Neg(amt).FloatString(security.Precision)
s1.Status = Imported s1.Status = models.Imported
s2.Status = Imported s2.Status = models.Imported
s1.AccountId = account.AccountId s1.AccountId = account.AccountId
s2.AccountId = -1 s2.AccountId = -1
@ -122,12 +122,12 @@ func (i *OFXImport) importOFXBank(stmt *ofxgo.StatementResponse) error {
return err return err
} }
account := Account{ account := models.Account{
AccountId: int64(len(i.Accounts) + 1), AccountId: int64(len(i.Accounts) + 1),
ExternalAccountId: stmt.BankAcctFrom.AcctID.String(), ExternalAccountId: stmt.BankAcctFrom.AcctID.String(),
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
ParentAccountId: -1, ParentAccountId: -1,
Type: Bank, Type: models.Bank,
} }
if stmt.BankTranList != nil { if stmt.BankTranList != nil {
@ -149,12 +149,12 @@ func (i *OFXImport) importOFXCC(stmt *ofxgo.CCStatementResponse) error {
return err return err
} }
account := Account{ account := models.Account{
AccountId: int64(len(i.Accounts) + 1), AccountId: int64(len(i.Accounts) + 1),
ExternalAccountId: stmt.CCAcctFrom.AcctID.String(), ExternalAccountId: stmt.CCAcctFrom.AcctID.String(),
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
ParentAccountId: -1, ParentAccountId: -1,
Type: Liability, Type: models.Liability,
} }
i.Accounts = append(i.Accounts, account) i.Accounts = append(i.Accounts, account)
@ -208,14 +208,14 @@ func (i *OFXImport) importSecurities(seclist *ofxgo.SecurityList) error {
return nil return nil
} }
func (i *OFXImport) GetInvTran(invtran *ofxgo.InvTran) Transaction { func (i *OFXImport) GetInvTran(invtran *ofxgo.InvTran) models.Transaction {
var t Transaction var t models.Transaction
t.Description = string(invtran.Memo) t.Description = string(invtran.Memo)
t.Date = invtran.DtTrade.UTC() t.Date = invtran.DtTrade.UTC()
return t 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) t := i.GetInvTran(&buy.InvTran)
security, err := i.GetSecurityAlternateId(string(buy.SecID.UniqueID), models.Stock) 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 { if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Commission, ImportSplitType: models.Commission,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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 { if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Taxes, ImportSplitType: models.Taxes,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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 { if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Fees, ImportSplitType: models.Fees,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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 { if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Load, ImportSplitType: models.Load,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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), Amount: load.FloatString(curdef.Precision),
}) })
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
Memo: memo, Memo: memo,
Amount: total.FloatString(curdef.Precision), Amount: total.FloatString(curdef.Precision),
}) })
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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 var units big.Rat
units.Abs(&buy.Units.Rat) units.Abs(&buy.Units.Rat)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: SubAccount, ImportSplitType: models.SubAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), 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), Amount: units.FloatString(security.Precision),
}) })
units.Neg(&units) units.Neg(&units)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + buy.InvTran.FiTID.String(), RemoteId: "ofx:" + buy.InvTran.FiTID.String(),
@ -349,7 +349,7 @@ func (i *OFXImport) GetInvBuyTran(buy *ofxgo.InvBuy, curdef *models.Security, ac
return &t, nil 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) t := i.GetInvTran(&income.InvTran)
security, err := i.GetSecurityAlternateId(string(income.SecID.UniqueID), models.Stock) 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) total.Mul(&total, &income.Currency.CurRate.Rat)
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + income.InvTran.FiTID.String(), 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), Amount: total.FloatString(curdef.Precision),
}) })
total.Neg(&total) total.Neg(&total)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: IncomeAccount, ImportSplitType: models.IncomeAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + income.InvTran.FiTID.String(), RemoteId: "ofx:" + income.InvTran.FiTID.String(),
@ -395,7 +395,7 @@ func (i *OFXImport) GetIncomeTran(income *ofxgo.Income, curdef *models.Security,
return &t, nil 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) t := i.GetInvTran(&expense.InvTran)
security, err := i.GetSecurityAlternateId(string(expense.SecID.UniqueID), models.Stock) 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) total.Mul(&total, &expense.Currency.CurRate.Rat)
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + expense.InvTran.FiTID.String(), RemoteId: "ofx:" + expense.InvTran.FiTID.String(),
@ -426,10 +426,10 @@ func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models.
Amount: total.FloatString(curdef.Precision), Amount: total.FloatString(curdef.Precision),
}) })
total.Neg(&total) total.Neg(&total)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ExpenseAccount, ImportSplitType: models.ExpenseAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + expense.InvTran.FiTID.String(), RemoteId: "ofx:" + expense.InvTran.FiTID.String(),
@ -440,7 +440,7 @@ func (i *OFXImport) GetInvExpenseTran(expense *ofxgo.InvExpense, curdef *models.
return &t, nil 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) t := i.GetInvTran(&marginint.InvTran)
memo := string(marginint.InvTran.Memo) memo := string(marginint.InvTran.Memo)
@ -454,10 +454,10 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde
total.Mul(&total, &marginint.Currency.CurRate.Rat) total.Mul(&total, &marginint.Currency.CurRate.Rat)
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + marginint.InvTran.FiTID.String(), RemoteId: "ofx:" + marginint.InvTran.FiTID.String(),
@ -465,10 +465,10 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde
Amount: total.FloatString(curdef.Precision), Amount: total.FloatString(curdef.Precision),
}) })
total.Neg(&total) total.Neg(&total)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: IncomeAccount, ImportSplitType: models.IncomeAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + marginint.InvTran.FiTID.String(), RemoteId: "ofx:" + marginint.InvTran.FiTID.String(),
@ -479,7 +479,7 @@ func (i *OFXImport) GetMarginInterestTran(marginint *ofxgo.MarginInterest, curde
return &t, nil 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) t := i.GetInvTran(&reinvest.InvTran)
security, err := i.GetSecurityAlternateId(string(reinvest.SecID.UniqueID), models.Stock) 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 { if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Commission, ImportSplitType: models.Commission,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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 { if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Taxes, ImportSplitType: models.Taxes,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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 { if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Fees, ImportSplitType: models.Fees,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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 { if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Load, ImportSplitType: models.Load,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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), Amount: load.FloatString(curdef.Precision),
}) })
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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), Amount: total.FloatString(curdef.Precision),
}) })
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: IncomeAccount, ImportSplitType: models.IncomeAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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), Amount: total.FloatString(curdef.Precision),
}) })
total.Neg(&total) total.Neg(&total)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
Memo: memo, Memo: memo,
Amount: total.FloatString(curdef.Precision), Amount: total.FloatString(curdef.Precision),
}) })
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
@ -610,10 +610,10 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec
var units big.Rat var units big.Rat
units.Abs(&reinvest.Units.Rat) units.Abs(&reinvest.Units.Rat)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: SubAccount, ImportSplitType: models.SubAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), 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), Amount: units.FloatString(security.Precision),
}) })
units.Neg(&units) units.Neg(&units)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(), RemoteId: "ofx:" + reinvest.InvTran.FiTID.String(),
@ -635,7 +635,7 @@ func (i *OFXImport) GetReinvestTran(reinvest *ofxgo.Reinvest, curdef *models.Sec
return &t, nil 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) t := i.GetInvTran(&retofcap.InvTran)
security, err := i.GetSecurityAlternateId(string(retofcap.SecID.UniqueID), models.Stock) 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) total.Mul(&total, &retofcap.Currency.CurRate.Rat)
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + retofcap.InvTran.FiTID.String(), 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), Amount: total.FloatString(curdef.Precision),
}) })
total.Neg(&total) total.Neg(&total)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: IncomeAccount, ImportSplitType: models.IncomeAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + retofcap.InvTran.FiTID.String(), RemoteId: "ofx:" + retofcap.InvTran.FiTID.String(),
@ -680,7 +680,7 @@ func (i *OFXImport) GetRetOfCapTran(retofcap *ofxgo.RetOfCap, curdef *models.Sec
return &t, nil 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) t := i.GetInvTran(&sell.InvTran)
security, err := i.GetSecurityAlternateId(string(sell.SecID.UniqueID), models.Stock) 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 { if num := commission.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Commission, ImportSplitType: models.Commission,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), 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 { if num := taxes.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Taxes, ImportSplitType: models.Taxes,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), 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 { if num := fees.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Fees, ImportSplitType: models.Fees,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), 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 { if num := load.Num(); !num.IsInt64() || num.Int64() != 0 {
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: Load, ImportSplitType: models.Load,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), 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), Amount: load.FloatString(curdef.Precision),
}) })
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ImportAccount, ImportSplitType: models.ImportAccount,
AccountId: account.AccountId, AccountId: account.AccountId,
SecurityId: -1, SecurityId: -1,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
Memo: memo, Memo: memo,
Amount: total.FloatString(curdef.Precision), Amount: total.FloatString(curdef.Precision),
}) })
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: curdef.SecurityId, SecurityId: curdef.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
@ -792,10 +792,10 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security,
var units big.Rat var units big.Rat
units.Abs(&sell.Units.Rat) units.Abs(&sell.Units.Rat)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: TradingAccount, ImportSplitType: models.TradingAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), 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), Amount: units.FloatString(security.Precision),
}) })
units.Neg(&units) units.Neg(&units)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: SubAccount, ImportSplitType: models.SubAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + sell.InvTran.FiTID.String(), RemoteId: "ofx:" + sell.InvTran.FiTID.String(),
@ -817,7 +817,7 @@ func (i *OFXImport) GetInvSellTran(sell *ofxgo.InvSell, curdef *models.Security,
return &t, nil 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) t := i.GetInvTran(&transfer.InvTran)
security, err := i.GetSecurityAlternateId(string(transfer.SecID.UniqueID), models.Stock) 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) units.Neg(&transfer.Units.Rat)
} }
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: SubAccount, ImportSplitType: models.SubAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + transfer.InvTran.FiTID.String(), RemoteId: "ofx:" + transfer.InvTran.FiTID.String(),
@ -845,10 +845,10 @@ func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account)
Amount: units.FloatString(security.Precision), Amount: units.FloatString(security.Precision),
}) })
units.Neg(&units) units.Neg(&units)
t.Splits = append(t.Splits, &Split{ t.Splits = append(t.Splits, &models.Split{
// TODO ReversalFiTID? // TODO ReversalFiTID?
Status: Imported, Status: models.Imported,
ImportSplitType: ExternalAccount, ImportSplitType: models.ExternalAccount,
AccountId: -1, AccountId: -1,
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
RemoteId: "ofx:" + transfer.InvTran.FiTID.String(), RemoteId: "ofx:" + transfer.InvTran.FiTID.String(),
@ -859,12 +859,12 @@ func (i *OFXImport) GetTransferTran(transfer *ofxgo.Transfer, account *Account)
return &t, nil 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)) { if curdef.SecurityId < 1 || curdef.SecurityId > int64(len(i.Securities)) {
return errors.New("Internal error: security index not found in OFX import\n") return errors.New("Internal error: security index not found in OFX import\n")
} }
var t *Transaction var t *models.Transaction
var err error var err error
if tran, ok := (*invtran).(ofxgo.BuyDebt); ok { if tran, ok := (*invtran).(ofxgo.BuyDebt); ok {
t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account) t, err = i.GetInvBuyTran(&tran.InvBuy, curdef, account)
@ -926,12 +926,12 @@ func (i *OFXImport) importOFXInv(stmt *ofxgo.InvStatementResponse) error {
return err return err
} }
account := Account{ account := models.Account{
AccountId: int64(len(i.Accounts) + 1), AccountId: int64(len(i.Accounts) + 1),
ExternalAccountId: stmt.InvAcctFrom.AcctID.String(), ExternalAccountId: stmt.InvAcctFrom.AcctID.String(),
SecurityId: security.SecurityId, SecurityId: security.SecurityId,
ParentAccountId: -1, ParentAccountId: -1,
Type: Investment, Type: models.Investment,
} }
i.Accounts = append(i.Accounts, account) i.Accounts = append(i.Accounts, account)

View File

@ -2,7 +2,6 @@ package handlers_test
import ( import (
"fmt" "fmt"
"github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models" "github.com/aclindsa/moneygo/internal/models"
"net/http" "net/http"
"strconv" "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) 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) accounts, err := getAccounts(client)
if err != nil { if err != nil {
return nil, err return nil, err
@ -105,11 +104,11 @@ func TestImportOFX401kMutualFunds(t *testing.T) {
t.Fatalf("Error removing default security: %s\n", err) t.Fatalf("Error removing default security: %s\n", err)
} }
account := &handlers.Account{ account := &models.Account{
SecurityId: d.securities[0].SecurityId, SecurityId: d.securities[0].SecurityId,
UserId: d.users[0].UserId, UserId: d.users[0].UserId,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Investment, Type: models.Investment,
Name: "401k", Name: "401k",
} }
@ -130,14 +129,14 @@ func TestImportOFX401kMutualFunds(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 security: %s\n", err) 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 { if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 trading account: %s\n", err) t.Fatalf("Error finding VANGUARD TARGET 2045 trading account: %s\n", err)
} }
accountBalanceHelper(t, d.clients[0], tradingaccount, "-3.35400") accountBalanceHelper(t, d.clients[0], tradingaccount, "-3.35400")
// Ensure actual holding account was created and in the correct place // 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 { if err != nil {
t.Fatalf("Error finding VANGUARD TARGET 2045 investment account: %s\n", err) 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 // Create the brokerage account
account := &handlers.Account{ account := &models.Account{
SecurityId: d.securities[0].SecurityId, SecurityId: d.securities[0].SecurityId,
UserId: d.users[0].UserId, UserId: d.users[0].UserId,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Investment, Type: models.Investment,
Name: "Personal Brokerage", Name: "Personal Brokerage",
} }
@ -185,7 +184,7 @@ func TestImportOFXBrokerage(t *testing.T) {
// Make sure the USD trading account was created and has the right // Make sure the USD trading account was created and has the right
// value // 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 { if err != nil {
t.Fatalf("Error finding USD trading account: %s\n", err) 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) 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 { if err != nil {
t.Fatalf("Error finding trading account: %s\n", err) t.Fatalf("Error finding trading account: %s\n", err)
} }
accountBalanceHelper(t, d.clients[0], account, check.Balance) 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 { if err != nil {
t.Fatalf("Error finding trading account: %s\n", err) t.Fatalf("Error finding trading account: %s\n", err)
} }

View File

@ -1,6 +1,7 @@
package handlers package handlers
import ( import (
"github.com/aclindsa/moneygo/internal/models"
"github.com/yuin/gopher-lua" "github.com/yuin/gopher-lua"
) )
@ -59,7 +60,7 @@ func luaPrice__index(L *lua.LState) int {
} }
L.Push(SecurityToLua(L, c)) L.Push(SecurityToLua(L, c))
case "Value", "value": case "Value", "value":
amt, err := GetBigAmount(p.Value) amt, err := models.GetBigAmount(p.Value)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -39,8 +39,8 @@ type TestData struct {
clients []*http.Client clients []*http.Client
securities []models.Security securities []models.Security
prices []handlers.Price prices []handlers.Price
accounts []handlers.Account // accounts must appear after their parents in this slice accounts []models.Account // accounts must appear after their parents in this slice
transactions []handlers.Transaction transactions []models.Transaction
reports []handlers.Report reports []handlers.Report
tabulations []handlers.Tabulation tabulations []handlers.Tabulation
} }
@ -113,7 +113,7 @@ func (t *TestData) Initialize() (*TestData, error) {
} }
for i, transaction := range t.transactions { for i, transaction := range t.transactions {
transaction.Splits = []*handlers.Split{} transaction.Splits = []*models.Split{}
for _, s := range t.transactions[i].Splits { for _, s := range t.transactions[i].Splits {
// Make a copy of the split since Splits is a slice of pointers so // Make a copy of the split since Splits is a slice of pointers so
// copying the transaction doesn't // copying the transaction doesn't
@ -246,78 +246,78 @@ var data = []TestData{
RemoteId: "USDEUR819298714", RemoteId: "USDEUR819298714",
}, },
}, },
accounts: []handlers.Account{ accounts: []models.Account{
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Asset, Type: models.Asset,
Name: "Assets", Name: "Assets",
}, },
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: 0, ParentAccountId: 0,
Type: handlers.Bank, Type: models.Bank,
Name: "Credit Union Checking", Name: "Credit Union Checking",
}, },
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Expense, Type: models.Expense,
Name: "Expenses", Name: "Expenses",
}, },
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: 2, ParentAccountId: 2,
Type: handlers.Expense, Type: models.Expense,
Name: "Groceries", Name: "Groceries",
}, },
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: 2, ParentAccountId: 2,
Type: handlers.Expense, Type: models.Expense,
Name: "Cable", Name: "Cable",
}, },
{ {
UserId: 1, UserId: 1,
SecurityId: 2, SecurityId: 2,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Asset, Type: models.Asset,
Name: "Assets", Name: "Assets",
}, },
{ {
UserId: 1, UserId: 1,
SecurityId: 2, SecurityId: 2,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Expense, Type: models.Expense,
Name: "Expenses", Name: "Expenses",
}, },
{ {
UserId: 0, UserId: 0,
SecurityId: 0, SecurityId: 0,
ParentAccountId: -1, ParentAccountId: -1,
Type: handlers.Liability, Type: models.Liability,
Name: "Credit Card", Name: "Credit Card",
}, },
}, },
transactions: []handlers.Transaction{ transactions: []models.Transaction{
{ {
UserId: 0, UserId: 0,
Description: "weekly groceries", Description: "weekly groceries",
Date: time.Date(2017, time.October, 15, 1, 16, 59, 0, time.UTC), 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, AccountId: 1,
SecurityId: -1, SecurityId: -1,
Amount: "-5.6", Amount: "-5.6",
}, },
{ {
Status: handlers.Reconciled, Status: models.Reconciled,
AccountId: 3, AccountId: 3,
SecurityId: -1, SecurityId: -1,
Amount: "5.6", Amount: "5.6",
@ -328,15 +328,15 @@ var data = []TestData{
UserId: 0, UserId: 0,
Description: "weekly groceries", Description: "weekly groceries",
Date: time.Date(2017, time.October, 31, 19, 10, 14, 0, time.UTC), 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, AccountId: 1,
SecurityId: -1, SecurityId: -1,
Amount: "-81.59", Amount: "-81.59",
}, },
{ {
Status: handlers.Reconciled, Status: models.Reconciled,
AccountId: 3, AccountId: 3,
SecurityId: -1, SecurityId: -1,
Amount: "81.59", Amount: "81.59",
@ -347,15 +347,15 @@ var data = []TestData{
UserId: 0, UserId: 0,
Description: "Cable", Description: "Cable",
Date: time.Date(2017, time.September, 2, 0, 00, 00, 0, time.UTC), 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, AccountId: 1,
SecurityId: -1, SecurityId: -1,
Amount: "-39.99", Amount: "-39.99",
}, },
{ {
Status: handlers.Entered, Status: models.Entered,
AccountId: 4, AccountId: 4,
SecurityId: -1, SecurityId: -1,
Amount: "39.99", Amount: "39.99",
@ -366,15 +366,15 @@ var data = []TestData{
UserId: 1, UserId: 1,
Description: "Gas", Description: "Gas",
Date: time.Date(2017, time.November, 1, 13, 19, 50, 0, time.UTC), 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, AccountId: 5,
SecurityId: -1, SecurityId: -1,
Amount: "-24.56", Amount: "-24.56",
}, },
{ {
Status: handlers.Entered, Status: models.Entered,
AccountId: 6, AccountId: 6,
SecurityId: -1, SecurityId: -1,
Amount: "24.56", Amount: "24.56",

View File

@ -1,7 +1,6 @@
package handlers package handlers
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/aclindsa/moneygo/internal/models" "github.com/aclindsa/moneygo/internal/models"
@ -10,141 +9,17 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings"
"time" "time"
) )
// Split.Status func SplitAlreadyImported(tx *Tx, s *models.Split) (bool, error) {
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) {
count, err := tx.SelectInt("SELECT COUNT(*) from splits where RemoteId=? and AccountId=?", s.RemoteId, s.AccountId) count, err := tx.SelectInt("SELECT COUNT(*) from splits where RemoteId=? and AccountId=?", s.RemoteId, s.AccountId)
return count == 1, err 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 // Return a map of security ID's to big.Rat's containing the amount that
// security is imbalanced by // 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) sums := make(map[int64]big.Rat)
if !t.Valid() { if !t.Valid() {
@ -155,7 +30,7 @@ func (t *Transaction) GetImbalances(tx *Tx) (map[int64]big.Rat, error) {
securityid := t.Splits[i].SecurityId securityid := t.Splits[i].SecurityId
if t.Splits[i].AccountId != -1 { if t.Splits[i].AccountId != -1 {
var err error var err error
var account *Account var account *models.Account
account, err = GetAccount(tx, t.Splits[i].AccountId, t.UserId) account, err = GetAccount(tx, t.Splits[i].AccountId, t.UserId)
if err != nil { if err != nil {
return nil, err 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, // Returns true if all securities contained in this transaction are balanced,
// false otherwise // false otherwise
func (t *Transaction) Balanced(tx *Tx) (bool, error) { func TransactionBalanced(tx *Tx, t *models.Transaction) (bool, error) {
var zero big.Rat var zero big.Rat
sums, err := t.GetImbalances(tx) sums, err := GetTransactionImbalances(tx, t)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -188,8 +63,8 @@ func (t *Transaction) Balanced(tx *Tx) (bool, error) {
return true, nil return true, nil
} }
func GetTransaction(tx *Tx, transactionid int64, userid int64) (*Transaction, error) { func GetTransaction(tx *Tx, transactionid int64, userid int64) (*models.Transaction, error) {
var t Transaction var t models.Transaction
err := tx.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid) err := tx.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid)
if err != nil { if err != nil {
@ -204,8 +79,8 @@ func GetTransaction(tx *Tx, transactionid int64, userid int64) (*Transaction, er
return &t, nil return &t, nil
} }
func GetTransactions(tx *Tx, userid int64) (*[]Transaction, error) { func GetTransactions(tx *Tx, userid int64) (*[]models.Transaction, error) {
var transactions []Transaction var transactions []models.Transaction
_, err := tx.Select(&transactions, "SELECT * from transactions where UserId=?", userid) _, err := tx.Select(&transactions, "SELECT * from transactions where UserId=?", userid)
if err != nil { if err != nil {
@ -246,7 +121,7 @@ func (ame AccountMissingError) Error() string {
return "Account missing" 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 // Map of any accounts with transaction splits being added
a_map := make(map[int64]bool) a_map := make(map[int64]bool)
for i := range t.Splits { for i := range t.Splits {
@ -296,8 +171,8 @@ func InsertTransaction(tx *Tx, t *Transaction, user *models.User) error {
return nil return nil
} }
func UpdateTransaction(tx *Tx, t *Transaction, user *models.User) error { func UpdateTransaction(tx *Tx, t *models.Transaction, user *models.User) error {
var existing_splits []*Split var existing_splits []*models.Split
_, err := tx.Select(&existing_splits, "SELECT * from splits where TransactionId=?", t.TransactionId) _, err := tx.Select(&existing_splits, "SELECT * from splits where TransactionId=?", t.TransactionId)
if err != nil { if err != nil {
@ -373,7 +248,7 @@ func UpdateTransaction(tx *Tx, t *Transaction, user *models.User) error {
return nil 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 var accountids []int64
_, err := tx.Select(&accountids, "SELECT DISTINCT AccountId FROM splits WHERE TransactionId=? AND AccountId != -1", t.TransactionId) _, err := tx.Select(&accountids, "SELECT DISTINCT AccountId FROM splits WHERE TransactionId=? AND AccountId != -1", t.TransactionId)
if err != nil { if err != nil {
@ -408,7 +283,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter
} }
if r.Method == "POST" { if r.Method == "POST" {
var transaction Transaction var transaction models.Transaction
if err := ReadJSON(r, &transaction); err != nil { if err := ReadJSON(r, &transaction); err != nil {
return NewError(3 /*Invalid Request*/) 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 { if err != nil {
return NewError(999 /*Internal Error*/) return NewError(999 /*Internal Error*/)
} }
@ -449,7 +324,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter
} else if r.Method == "GET" { } else if r.Method == "GET" {
if context.LastLevel() { if context.LastLevel() {
//Return all Transactions //Return all Transactions
var al TransactionList var al models.TransactionList
transactions, err := GetTransactions(context.Tx, user.UserId) transactions, err := GetTransactions(context.Tx, user.UserId)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
@ -475,13 +350,13 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter
return NewError(3 /*Invalid Request*/) return NewError(3 /*Invalid Request*/)
} }
if r.Method == "PUT" { if r.Method == "PUT" {
var transaction Transaction var transaction models.Transaction
if err := ReadJSON(r, &transaction); err != nil || transaction.TransactionId != transactionid { if err := ReadJSON(r, &transaction); err != nil || transaction.TransactionId != transactionid {
return NewError(3 /*Invalid Request*/) return NewError(3 /*Invalid Request*/)
} }
transaction.UserId = user.UserId transaction.UserId = user.UserId
balanced, err := transaction.Balanced(context.Tx) balanced, err := TransactionBalanced(context.Tx, &transaction)
if err != nil { if err != nil {
log.Print(err) log.Print(err)
return NewError(999 /*Internal Error*/) return NewError(999 /*Internal Error*/)
@ -526,7 +401,7 @@ func TransactionHandler(r *http.Request, context *Context) ResponseWriterWriter
return NewError(3 /*Invalid Request*/) 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 var pageDifference, tmp big.Rat
for i := range transactions { for i := range transactions {
_, err := tx.Select(&transactions[i].Splits, "SELECT * FROM splits where TransactionId=?", transactions[i].TransactionId) _, 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 // an ending balance
for j := range transactions[i].Splits { for j := range transactions[i].Splits {
if transactions[i].Splits[j].AccountId == accountid { 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 { if err != nil {
return nil, err 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) { 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=?" 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) _, 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 var balance, tmp big.Rat
for _, s := range splits { for _, s := range splits {
rat_amount, err := GetBigAmount(s.Amount) rat_amount, err := models.GetBigAmount(s.Amount)
if err != nil { if err != nil {
return nil, err 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 // 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) { 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 < ?" 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) _, 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 var balance, tmp big.Rat
for _, s := range splits { for _, s := range splits {
rat_amount, err := GetBigAmount(s.Amount) rat_amount, err := models.GetBigAmount(s.Amount)
if err != nil { if err != nil {
return nil, err 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) { 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 < ?" 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) _, 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 var balance, tmp big.Rat
for _, s := range splits { for _, s := range splits {
rat_amount, err := GetBigAmount(s.Amount) rat_amount, err := models.GetBigAmount(s.Amount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -617,9 +492,9 @@ func GetAccountBalanceDateRange(tx *Tx, user *models.User, accountid int64, begi
return &balance, nil return &balance, nil
} }
func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort string, page uint64, limit uint64) (*AccountTransactionsList, error) { func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort string, page uint64, limit uint64) (*models.AccountTransactionsList, error) {
var transactions []Transaction var transactions []models.Transaction
var atl AccountTransactionsList var atl models.AccountTransactionsList
var sqlsort, balanceLimitOffset string var sqlsort, balanceLimitOffset string
var balanceLimitOffsetArg uint64 var balanceLimitOffsetArg uint64
@ -685,7 +560,7 @@ func GetAccountTransactions(tx *Tx, user *models.User, accountid int64, sort str
var tmp, balance big.Rat var tmp, balance big.Rat
for _, amount := range amounts { for _, amount := range amounts {
rat_amount, err := GetBigAmount(amount) rat_amount, err := models.GetBigAmount(amount)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -3,6 +3,7 @@ package handlers_test
import ( import (
"fmt" "fmt"
"github.com/aclindsa/moneygo/internal/handlers" "github.com/aclindsa/moneygo/internal/handlers"
"github.com/aclindsa/moneygo/internal/models"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@ -10,14 +11,14 @@ import (
"time" "time"
) )
func createTransaction(client *http.Client, transaction *handlers.Transaction) (*handlers.Transaction, error) { func createTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) {
var s handlers.Transaction var s models.Transaction
err := create(client, transaction, &s, "/v1/transactions/") err := create(client, transaction, &s, "/v1/transactions/")
return &s, err return &s, err
} }
func getTransaction(client *http.Client, transactionid int64) (*handlers.Transaction, error) { func getTransaction(client *http.Client, transactionid int64) (*models.Transaction, error) {
var s handlers.Transaction var s models.Transaction
err := read(client, &s, "/v1/transactions/"+strconv.FormatInt(transactionid, 10)) err := read(client, &s, "/v1/transactions/"+strconv.FormatInt(transactionid, 10))
if err != nil { if err != nil {
return nil, err return nil, err
@ -25,8 +26,8 @@ func getTransaction(client *http.Client, transactionid int64) (*handlers.Transac
return &s, nil return &s, nil
} }
func getTransactions(client *http.Client) (*handlers.TransactionList, error) { func getTransactions(client *http.Client) (*models.TransactionList, error) {
var tl handlers.TransactionList var tl models.TransactionList
err := read(client, &tl, "/v1/transactions/") err := read(client, &tl, "/v1/transactions/")
if err != nil { if err != nil {
return nil, err return nil, err
@ -34,8 +35,8 @@ func getTransactions(client *http.Client) (*handlers.TransactionList, error) {
return &tl, nil return &tl, nil
} }
func getAccountTransactions(client *http.Client, accountid, page, limit int64, sort string) (*handlers.AccountTransactionsList, error) { func getAccountTransactions(client *http.Client, accountid, page, limit int64, sort string) (*models.AccountTransactionsList, error) {
var atl handlers.AccountTransactionsList var atl models.AccountTransactionsList
params := url.Values{} params := url.Values{}
query := fmt.Sprintf("/v1/accounts/%d/transactions/", accountid) 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 return &atl, nil
} }
func updateTransaction(client *http.Client, transaction *handlers.Transaction) (*handlers.Transaction, error) { func updateTransaction(client *http.Client, transaction *models.Transaction) (*models.Transaction, error) {
var s handlers.Transaction var s models.Transaction
err := update(client, transaction, &s, "/v1/transactions/"+strconv.FormatInt(transaction.TransactionId, 10)) err := update(client, transaction, &s, "/v1/transactions/"+strconv.FormatInt(transaction.TransactionId, 10))
if err != nil { if err != nil {
return nil, err return nil, err
@ -66,7 +67,7 @@ func updateTransaction(client *http.Client, transaction *handlers.Transaction) (
return &s, nil 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)) err := remove(client, "/v1/transactions/"+strconv.FormatInt(s.TransactionId, 10))
if err != nil { if err != nil {
return err return err
@ -74,7 +75,7 @@ func deleteTransaction(client *http.Client, s *handlers.Transaction) error {
return nil 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() t.Helper()
if tran.TransactionId == 0 { 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() t.Helper()
accountMap := make(map[int64]*handlers.Account) accountMap := make(map[int64]*models.Account)
for _, split := range tran.Splits { for _, split := range tran.Splits {
account, err := getAccount(client, split.AccountId) account, err := getAccount(client, split.AccountId)
if err != nil { if err != nil {
@ -149,7 +150,7 @@ func getAccountVersionMap(t *testing.T, client *http.Client, tran *handlers.Tran
return accountMap 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 { for _, split := range tran.Splits {
account, err := getAccount(client, split.AccountId) account, err := getAccount(client, split.AccountId)
if err != nil { if err != nil {
@ -177,19 +178,19 @@ func TestCreateTransaction(t *testing.T) {
} }
// Don't allow imbalanced transactions // Don't allow imbalanced transactions
tran := handlers.Transaction{ tran := models.Transaction{
UserId: d.users[0].UserId, UserId: d.users[0].UserId,
Description: "Imbalanced", Description: "Imbalanced",
Date: time.Date(2017, time.September, 1, 0, 00, 00, 0, time.UTC), 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, AccountId: d.accounts[1].AccountId,
SecurityId: -1, SecurityId: -1,
Amount: "-39.98", Amount: "-39.98",
}, },
{ {
Status: handlers.Entered, Status: models.Entered,
AccountId: d.accounts[4].AccountId, AccountId: d.accounts[4].AccountId,
SecurityId: -1, SecurityId: -1,
Amount: "39.99", Amount: "39.99",
@ -209,7 +210,7 @@ func TestCreateTransaction(t *testing.T) {
} }
// Don't allow transactions with 0 splits // Don't allow transactions with 0 splits
tran.Splits = []*handlers.Split{} tran.Splits = []*models.Split{}
_, err = createTransaction(d.clients[0], &tran) _, err = createTransaction(d.clients[0], &tran)
if err == nil { if err == nil {
t.Fatalf("Expected error creating with zero splits") t.Fatalf("Expected error creating with zero splits")
@ -316,9 +317,9 @@ func TestUpdateTransaction(t *testing.T) {
ensureTransactionsMatch(t, &curr, tran, nil, true, true) ensureTransactionsMatch(t, &curr, tran, nil, true, true)
tran.Splits = []*handlers.Split{} tran.Splits = []*models.Split{}
for _, s := range curr.Splits { for _, s := range curr.Splits {
var split handlers.Split var split models.Split
split = *s split = *s
tran.Splits = append(tran.Splits, &split) tran.Splits = append(tran.Splits, &split)
} }
@ -346,7 +347,7 @@ func TestUpdateTransaction(t *testing.T) {
} }
// Don't allow transactions with 0 splits // Don't allow transactions with 0 splits
tran.Splits = []*handlers.Split{} tran.Splits = []*models.Split{}
_, err = updateTransaction(d.clients[orig.UserId], tran) _, err = updateTransaction(d.clients[orig.UserId], tran)
if err == nil { if err == nil {
t.Fatalf("Expected error updating with zero splits") 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 { if account.UserId != d.users[0].UserId {
return return
} }
var transactions []handlers.Transaction var transactions []models.Transaction
var lastFetchCount int64 var lastFetchCount int64
for page := int64(0); page == 0 || lastFetchCount > 0; page++ { for page := int64(0); page == 0 || lastFetchCount > 0; page++ {

118
internal/models/accounts.go Normal file
View File

@ -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)
}

View File

@ -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
}