package handlers import ( "encoding/json" "errors" "gopkg.in/gorp.v1" "log" "net/http" "regexp" "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"` } var accountTransactionsRE *regexp.Regexp var accountImportRE *regexp.Regexp func init() { accountTransactionsRE = regexp.MustCompile(`^/account/[0-9]+/transactions/?$`) accountImportRE = regexp.MustCompile(`^/account/[0-9]+/import/[a-z]+/?$`) } 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) if err != nil { return nil, err } return &a, nil } func GetAccountTx(transaction *gorp.Transaction, accountid int64, userid int64) (*Account, error) { var a Account err := transaction.SelectOne(&a, "SELECT * from accounts where UserId=? AND AccountId=?", userid, accountid) if err != nil { return nil, err } return &a, nil } func GetAccounts(tx *Tx, userid int64) (*[]Account, error) { var accounts []Account _, err := tx.Select(&accounts, "SELECT * from accounts where UserId=?", userid) if err != nil { return nil, err } return &accounts, nil } // Get (and attempt to create if it doesn't exist). Matches on UserId, // SecurityId, Type, Name, and ParentAccountId func GetCreateAccountTx(transaction *gorp.Transaction, a Account) (*Account, error) { var accounts []Account var account Account // Try to find the top-level trading account _, err := transaction.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) if err != nil { return nil, err } if len(accounts) == 1 { account = accounts[0] } else { account.UserId = a.UserId account.SecurityId = a.SecurityId account.Type = a.Type account.Name = a.Name account.ParentAccountId = a.ParentAccountId err = transaction.Insert(&account) if err != nil { return nil, err } } return &account, nil } // Get (and attempt to create if it doesn't exist) the security/currency // trading account for the supplied security/currency func GetTradingAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) { var tradingAccount Account var account Account user, err := GetUserTx(transaction, userid) if err != nil { return nil, err } tradingAccount.UserId = userid tradingAccount.Type = Trading tradingAccount.Name = "Trading" tradingAccount.SecurityId = user.DefaultCurrency tradingAccount.ParentAccountId = -1 // Find/create the top-level trading account ta, err := GetCreateAccountTx(transaction, tradingAccount) if err != nil { return nil, err } security, err := GetSecurityTx(transaction, securityid, userid) if err != nil { return nil, err } account.UserId = userid account.Name = security.Name account.ParentAccountId = ta.AccountId account.SecurityId = securityid account.Type = Trading a, err := GetCreateAccountTx(transaction, account) if err != nil { return nil, err } return a, nil } // Get (and attempt to create if it doesn't exist) the security/currency // imbalance account for the supplied security/currency func GetImbalanceAccount(transaction *gorp.Transaction, userid int64, securityid int64) (*Account, error) { var imbalanceAccount Account var account Account xxxtemplate := FindSecurityTemplate("XXX", Currency) if xxxtemplate == nil { return nil, errors.New("Couldn't find XXX security template") } xxxsecurity, err := ImportGetCreateSecurity(transaction, userid, xxxtemplate) if err != nil { return nil, errors.New("Couldn't create XXX security") } imbalanceAccount.UserId = userid imbalanceAccount.Name = "Imbalances" imbalanceAccount.ParentAccountId = -1 imbalanceAccount.SecurityId = xxxsecurity.SecurityId imbalanceAccount.Type = Bank // Find/create the top-level trading account ia, err := GetCreateAccountTx(transaction, imbalanceAccount) if err != nil { return nil, err } security, err := GetSecurityTx(transaction, securityid, userid) if err != nil { return nil, err } account.UserId = userid account.Name = security.Name account.ParentAccountId = ia.AccountId account.SecurityId = securityid account.Type = Bank a, err := GetCreateAccountTx(transaction, account) if err != nil { return nil, err } return a, nil } type ParentAccountMissingError struct{} func (pame ParentAccountMissingError) Error() string { return "Parent account missing" } type TooMuchNestingError struct{} func (tmne TooMuchNestingError) Error() string { return "Too much nesting" } type CircularAccountsError struct{} func (cae CircularAccountsError) Error() string { return "Would result in circular account relationship" } func insertUpdateAccount(tx *Tx, a *Account, insert bool) error { found := make(map[int64]bool) if !insert { found[a.AccountId] = true } parentid := a.ParentAccountId depth := 0 for parentid != -1 { depth += 1 if depth > 100 { return TooMuchNestingError{} } var a Account err := tx.SelectOne(&a, "SELECT * from accounts where AccountId=?", parentid) if err != nil { return ParentAccountMissingError{} } // Insertion by itself can never result in circular dependencies if insert { break } found[parentid] = true parentid = a.ParentAccountId if _, ok := found[parentid]; ok { return CircularAccountsError{} } } if insert { err := tx.Insert(a) if err != nil { return err } } else { oldacct, err := GetAccountTx(tx, a.AccountId, a.UserId) if err != nil { return err } a.AccountVersion = oldacct.AccountVersion + 1 count, err := tx.Update(a) if err != nil { return err } if count != 1 { return errors.New("Updated more than one account") } } return nil } func InsertAccount(tx *Tx, a *Account) error { return insertUpdateAccount(tx, a, true) } func UpdateAccount(tx *Tx, a *Account) error { return insertUpdateAccount(tx, a, false) } func DeleteAccount(tx *Tx, a *Account) error { if a.ParentAccountId != -1 { // Re-parent splits to this account's parent account if this account isn't a root account _, err := tx.Exec("UPDATE splits SET AccountId=? WHERE AccountId=?", a.ParentAccountId, a.AccountId) if err != nil { return err } } else { // Delete splits if this account is a root account _, err := tx.Exec("DELETE FROM splits WHERE AccountId=?", a.AccountId) if err != nil { return err } } // Re-parent child accounts to this account's parent account _, err := tx.Exec("UPDATE accounts SET ParentAccountId=? WHERE ParentAccountId=?", a.ParentAccountId, a.AccountId) if err != nil { return err } count, err := tx.Delete(a) if err != nil { return err } if count != 1 { return errors.New("Was going to delete more than one account") } return nil } func AccountHandler(r *http.Request, tx *Tx) ResponseWriterWriter { user, err := GetUserFromSession(tx, r) if err != nil { return NewError(1 /*Not Signed In*/) } if r.Method == "POST" { // if URL looks like /account/[0-9]+/import, use the account // import handler if accountImportRE.MatchString(r.URL.Path) { var accountid int64 var importtype string n, err := GetURLPieces(r.URL.Path, "/account/%d/import/%s", &accountid, &importtype) if err != nil || n != 2 { log.Print(err) return NewError(999 /*Internal Error*/) } return AccountImportHandler(tx, r, user, accountid, importtype) } account_json := r.PostFormValue("account") if account_json == "" { return NewError(3 /*Invalid Request*/) } var account Account err := account.Read(account_json) if err != nil { return NewError(3 /*Invalid Request*/) } account.AccountId = -1 account.UserId = user.UserId account.AccountVersion = 0 security, err := GetSecurity(tx, account.SecurityId, user.UserId) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) } if security == nil { return NewError(3 /*Invalid Request*/) } err = InsertAccount(tx, &account) if err != nil { if _, ok := err.(ParentAccountMissingError); ok { return NewError(3 /*Invalid Request*/) } else { log.Print(err) return NewError(999 /*Internal Error*/) } } return ResponseWrapper{201, &account} } else if r.Method == "GET" { var accountid int64 n, err := GetURLPieces(r.URL.Path, "/account/%d", &accountid) if err != nil || n != 1 { //Return all Accounts var al AccountList accounts, err := GetAccounts(tx, user.UserId) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) } al.Accounts = accounts return &al } else { // if URL looks like /account/[0-9]+/transactions, use the account // transaction handler if accountTransactionsRE.MatchString(r.URL.Path) { return AccountTransactionsHandler(tx, r, user, accountid) } // Return Account with this Id account, err := GetAccount(tx, accountid, user.UserId) if err != nil { return NewError(3 /*Invalid Request*/) } return account } } else { accountid, err := GetURLID(r.URL.Path) if err != nil { return NewError(3 /*Invalid Request*/) } if r.Method == "PUT" { account_json := r.PostFormValue("account") if account_json == "" { return NewError(3 /*Invalid Request*/) } var account Account err := account.Read(account_json) if err != nil || account.AccountId != accountid { return NewError(3 /*Invalid Request*/) } account.UserId = user.UserId security, err := GetSecurity(tx, account.SecurityId, user.UserId) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) } if security == nil { return NewError(3 /*Invalid Request*/) } if account.ParentAccountId == account.AccountId { return NewError(3 /*Invalid Request*/) } err = UpdateAccount(tx, &account) if err != nil { if _, ok := err.(ParentAccountMissingError); ok { return NewError(3 /*Invalid Request*/) } else if _, ok := err.(CircularAccountsError); ok { return NewError(3 /*Invalid Request*/) } else { log.Print(err) return NewError(999 /*Internal Error*/) } } return &account } else if r.Method == "DELETE" { account, err := GetAccount(tx, accountid, user.UserId) if err != nil { return NewError(3 /*Invalid Request*/) } err = DeleteAccount(tx, account) if err != nil { log.Print(err) return NewError(999 /*Internal Error*/) } return SuccessWriter{} } } return NewError(3 /*Invalid Request*/) }