mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-12-27 16:02:27 -05:00
Aaron Lindsay
2ff1f47432
Simplify naming to remove "Tx" now that all handlers only have access to transactions anyway, and always use "tx" as the name of the variable representing the SQL transactions (to make it less likely to cause confusion with monetary transactions).
525 lines
12 KiB
Go
525 lines
12 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"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 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 GetCreateAccount(tx *Tx, a Account) (*Account, error) {
|
|
var accounts []Account
|
|
var account 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)
|
|
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 = tx.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(tx *Tx, userid int64, securityid int64) (*Account, error) {
|
|
var tradingAccount Account
|
|
var account Account
|
|
|
|
user, err := GetUser(tx, 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 := GetCreateAccount(tx, tradingAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
security, err := GetSecurity(tx, 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 := GetCreateAccount(tx, 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(tx *Tx, 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(tx, 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 := GetCreateAccount(tx, imbalanceAccount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
security, err := GetSecurity(tx, 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 := GetCreateAccount(tx, 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 := GetAccount(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*/)
|
|
}
|