2015-06-25 22:36:58 -04:00
package main
2015-06-27 17:46:06 -04:00
import (
"encoding/json"
"errors"
2015-07-11 08:58:36 -04:00
"gopkg.in/gorp.v1"
2015-06-27 17:46:06 -04:00
"log"
"net/http"
2015-07-11 08:58:36 -04:00
"regexp"
2015-06-27 17:46:06 -04:00
"strings"
)
2015-06-25 22:36:58 -04:00
const (
2015-07-04 08:23:57 -04:00
Bank int64 = 1
Cash = 2
Asset = 3
Liability = 4
Investment = 5
Income = 6
Expense = 7
2016-02-10 18:36:11 -05:00
Trading = 8
2015-06-25 22:36:58 -04:00
)
type Account struct {
2016-02-02 21:46:27 -05:00
AccountId int64
ExternalAccountId string
UserId int64
SecurityId int64
ParentAccountId int64 // -1 if this account is at the root
Type int64
Name string
2015-07-11 08:58:36 -04:00
// monotonically-increasing account transaction version number. Used for
// allowing a client to ensure they have a consistent version when paging
// through transactions.
2015-08-05 21:25:25 -04:00
AccountVersion int64 ` json:"Version" `
2015-06-27 17:46:06 -04:00
}
type AccountList struct {
Accounts * [ ] Account ` json:"accounts" `
}
2015-07-11 08:58:36 -04:00
var accountTransactionsRE * regexp . Regexp
2016-02-02 21:46:27 -05:00
var accountImportRE * regexp . Regexp
2015-07-11 08:58:36 -04:00
func init ( ) {
accountTransactionsRE = regexp . MustCompile ( ` ^/account/[0-9]+/transactions/?$ ` )
2016-02-02 21:46:27 -05:00
accountImportRE = regexp . MustCompile ( ` ^/account/[0-9]+/import/?$ ` )
2015-07-11 08:58:36 -04:00
}
2015-06-27 17:46:06 -04:00
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 GetAccount ( accountid int64 , userid int64 ) ( * Account , error ) {
var a Account
err := DB . SelectOne ( & a , "SELECT * from accounts where UserId=? AND AccountId=?" , userid , accountid )
if err != nil {
return nil , err
}
return & a , nil
}
2015-07-11 08:58:36 -04:00
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
}
2015-06-27 17:46:06 -04:00
func GetAccounts ( userid int64 ) ( * [ ] Account , error ) {
var accounts [ ] Account
_ , err := DB . Select ( & accounts , "SELECT * from accounts where UserId=?" , userid )
if err != nil {
return nil , err
}
return & accounts , nil
}
2016-02-10 18:36:11 -05:00
// Get (and attempt to create if it doesn't exist) the security/currency
// trading account for the supplied security/currency
func GetTradingAccount ( userid int64 , securityid int64 ) ( * Account , error ) {
var tradingAccounts [ ] Account //top-level 'Trading' account(s)
var tradingAccount Account
var accounts [ ] Account //second-level security-specific trading account(s)
var account Account
transaction , err := DB . Begin ( )
if err != nil {
return nil , err
}
// Try to find the top-level trading account
_ , err = transaction . Select ( & tradingAccounts , "SELECT * from accounts where UserId=? AND Name='Trading' AND ParentAccountId=-1 AND Type=? ORDER BY AccountId ASC LIMIT 1" , userid , Trading )
if err != nil {
transaction . Rollback ( )
return nil , err
}
if len ( tradingAccounts ) == 1 {
tradingAccount = tradingAccounts [ 0 ]
} else {
tradingAccount . UserId = userid
tradingAccount . Name = "Trading"
tradingAccount . ParentAccountId = - 1
tradingAccount . SecurityId = 840 /*USD*/ //FIXME SecurityId shouldn't matter for top-level trading account, but maybe we should grab the user's default
tradingAccount . Type = Trading
err = transaction . Insert ( & tradingAccount )
if err != nil {
transaction . Rollback ( )
return nil , err
}
}
// Now, try to find the security-specific trading account
_ , err = transaction . Select ( & accounts , "SELECT * from accounts where UserId=? AND SecurityId=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1" , userid , securityid , tradingAccount . AccountId )
if err != nil {
transaction . Rollback ( )
return nil , err
}
if len ( accounts ) == 1 {
account = accounts [ 0 ]
} else {
security := GetSecurity ( securityid )
account . UserId = userid
account . Name = security . Name
account . ParentAccountId = tradingAccount . AccountId
account . SecurityId = securityid
account . Type = Trading
err = transaction . Insert ( & account )
2016-02-11 05:53:44 -05:00
if err != nil {
transaction . Rollback ( )
return nil , err
}
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return nil , err
}
return & account , nil
}
// Get (and attempt to create if it doesn't exist) the security/currency
// imbalance account for the supplied security/currency
func GetImbalanceAccount ( userid int64 , securityid int64 ) ( * Account , error ) {
var imbalanceAccounts [ ] Account //top-level imbalance account(s)
var imbalanceAccount Account
var accounts [ ] Account //second-level security-specific imbalance account(s)
var account Account
transaction , err := DB . Begin ( )
if err != nil {
return nil , err
}
// Try to find the top-level imbalance account
_ , err = transaction . Select ( & imbalanceAccounts , "SELECT * from accounts where UserId=? AND Name='Imbalances' AND ParentAccountId=-1 AND Type=? ORDER BY AccountId ASC LIMIT 1" , userid , Bank )
if err != nil {
transaction . Rollback ( )
return nil , err
}
if len ( imbalanceAccounts ) == 1 {
imbalanceAccount = imbalanceAccounts [ 0 ]
} else {
imbalanceAccount . UserId = userid
imbalanceAccount . Name = "Imbalances"
imbalanceAccount . ParentAccountId = - 1
imbalanceAccount . SecurityId = 840 /*USD*/ //FIXME SecurityId shouldn't matter for top-level imbalance account, but maybe we should grab the user's default
imbalanceAccount . Type = Bank
err = transaction . Insert ( & imbalanceAccount )
if err != nil {
transaction . Rollback ( )
return nil , err
}
}
// Now, try to find the security-specific imbalances account
_ , err = transaction . Select ( & accounts , "SELECT * from accounts where UserId=? AND SecurityId=? AND ParentAccountId=? ORDER BY AccountId ASC LIMIT 1" , userid , securityid , imbalanceAccount . AccountId )
if err != nil {
transaction . Rollback ( )
return nil , err
}
if len ( accounts ) == 1 {
account = accounts [ 0 ]
} else {
security := GetSecurity ( securityid )
account . UserId = userid
account . Name = security . Name
account . ParentAccountId = imbalanceAccount . AccountId
account . SecurityId = securityid
account . Type = Bank
err = transaction . Insert ( & account )
2016-02-10 18:36:11 -05:00
if err != nil {
transaction . Rollback ( )
return nil , err
}
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return nil , err
}
return & account , nil
}
2015-06-27 17:46:06 -04:00
type ParentAccountMissingError struct { }
func ( pame ParentAccountMissingError ) Error ( ) string {
return "Parent account missing"
}
func insertUpdateAccount ( a * Account , insert bool ) error {
transaction , err := DB . Begin ( )
if err != nil {
return err
}
if a . ParentAccountId != - 1 {
existing , err := transaction . SelectInt ( "SELECT count(*) from accounts where AccountId=?" , a . ParentAccountId )
if err != nil {
transaction . Rollback ( )
return err
}
if existing != 1 {
transaction . Rollback ( )
return ParentAccountMissingError { }
}
}
if insert {
err = transaction . Insert ( a )
if err != nil {
transaction . Rollback ( )
return err
}
} else {
2015-07-11 08:58:36 -04:00
oldacct , err := GetAccountTx ( transaction , a . AccountId , a . UserId )
if err != nil {
transaction . Rollback ( )
return err
}
2015-08-05 21:25:25 -04:00
a . AccountVersion = oldacct . AccountVersion + 1
2015-07-11 08:58:36 -04:00
2015-06-27 17:46:06 -04:00
count , err := transaction . Update ( a )
if err != nil {
transaction . Rollback ( )
return err
}
if count != 1 {
transaction . Rollback ( )
return errors . New ( "Updated more than one account" )
}
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return err
}
return nil
}
func InsertAccount ( a * Account ) error {
return insertUpdateAccount ( a , true )
}
func UpdateAccount ( a * Account ) error {
return insertUpdateAccount ( a , false )
}
2015-06-29 07:25:48 -04:00
func DeleteAccount ( a * Account ) error {
transaction , err := DB . Begin ( )
if err != nil {
return err
}
2015-07-04 21:11:00 -04:00
if a . ParentAccountId != - 1 {
// Re-parent splits to this account's parent account if this account isn't a root account
_ , err = transaction . Exec ( "UPDATE splits SET AccountId=? WHERE AccountId=?" , a . ParentAccountId , a . AccountId )
if err != nil {
transaction . Rollback ( )
return err
}
} else {
// Delete splits if this account is a root account
_ , err = transaction . Exec ( "DELETE FROM splits WHERE AccountId=?" , a . AccountId )
if err != nil {
transaction . Rollback ( )
return err
}
2015-06-29 07:25:48 -04:00
}
// Re-parent child accounts to this account's parent account
_ , err = transaction . Exec ( "UPDATE accounts SET ParentAccountId=? WHERE ParentAccountId=?" , a . ParentAccountId , a . AccountId )
if err != nil {
transaction . Rollback ( )
return err
}
count , err := transaction . Delete ( a )
if err != nil {
transaction . Rollback ( )
return err
}
if count != 1 {
transaction . Rollback ( )
return errors . New ( "Was going to delete more than one account" )
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return err
}
return nil
}
2015-06-27 17:46:06 -04:00
func AccountHandler ( w http . ResponseWriter , r * http . Request ) {
user , err := GetUserFromSession ( r )
if err != nil {
WriteError ( w , 1 /*Not Signed In*/ )
return
}
if r . Method == "POST" {
2016-02-02 21:46:27 -05:00
// if URL looks like /account/[0-9]+/import, use the account
// import handler
if accountImportRE . MatchString ( r . URL . Path ) {
var accountid int64
n , err := GetURLPieces ( r . URL . Path , "/account/%d" , & accountid )
if err != nil || n != 1 {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
AccountImportHandler ( w , r , user , accountid )
return
}
2015-06-27 17:46:06 -04:00
account_json := r . PostFormValue ( "account" )
if account_json == "" {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
var account Account
err := account . Read ( account_json )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
account . AccountId = - 1
account . UserId = user . UserId
2015-08-05 21:25:25 -04:00
account . AccountVersion = 0
2015-06-27 17:46:06 -04:00
if GetSecurity ( account . SecurityId ) == nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
err = InsertAccount ( & account )
if err != nil {
if _ , ok := err . ( ParentAccountMissingError ) ; ok {
WriteError ( w , 3 /*Invalid Request*/ )
} else {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
}
return
}
WriteSuccess ( w )
} else if r . Method == "GET" {
2015-07-11 08:58:36 -04:00
var accountid int64
n , err := GetURLPieces ( r . URL . Path , "/account/%d" , & accountid )
if err != nil || n != 1 {
2015-06-27 17:46:06 -04:00
//Return all Accounts
var al AccountList
accounts , err := GetAccounts ( user . UserId )
if err != nil {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
al . Accounts = accounts
err = ( & al ) . Write ( w )
if err != nil {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
} else {
2015-07-11 08:58:36 -04:00
// if URL looks like /account/[0-9]+/transactions, use the account
// transaction handler
if accountTransactionsRE . MatchString ( r . URL . Path ) {
AccountTransactionsHandler ( w , r , user , accountid )
return
}
2015-06-29 07:25:48 -04:00
// Return Account with this Id
2015-06-27 17:46:06 -04:00
account , err := GetAccount ( accountid , user . UserId )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
2015-07-11 08:58:36 -04:00
2015-06-27 17:46:06 -04:00
err = account . Write ( w )
if err != nil {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
}
} else {
accountid , err := GetURLID ( r . URL . Path )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
if r . Method == "PUT" {
account_json := r . PostFormValue ( "account" )
if account_json == "" {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
var account Account
err := account . Read ( account_json )
if err != nil || account . AccountId != accountid {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
account . UserId = user . UserId
if GetSecurity ( account . SecurityId ) == nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
err = UpdateAccount ( & account )
if err != nil {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
WriteSuccess ( w )
} else if r . Method == "DELETE" {
accountid , err := GetURLID ( r . URL . Path )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
account , err := GetAccount ( accountid , user . UserId )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
2015-06-29 07:25:48 -04:00
err = DeleteAccount ( account )
if err != nil {
2015-06-27 17:46:06 -04:00
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
WriteSuccess ( w )
}
}
2015-06-25 22:36:58 -04:00
}