mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-11-13 02:30:05 -05:00
Aaron Lindsay
156b9aaf0c
This is part of an ongoing attempt to restructure the code to make it more 'testable'.
946 lines
22 KiB
Go
946 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"gopkg.in/gorp.v1"
|
|
"log"
|
|
"math/big"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Split.Status
|
|
const (
|
|
Imported int64 = 1
|
|
Entered = 2
|
|
Cleared = 3
|
|
Reconciled = 4
|
|
Voided = 5
|
|
)
|
|
|
|
// Split.ImportSplitType
|
|
const (
|
|
Default int64 = 0
|
|
ImportAccount = 1 // This split belongs to the main account being imported
|
|
SubAccount = 2 // This split belongs to a sub-account of that being imported
|
|
ExternalAccount = 3
|
|
TradingAccount = 4
|
|
Commission = 5
|
|
Taxes = 6
|
|
Fees = 7
|
|
Load = 8
|
|
IncomeAccount = 9
|
|
ExpenseAccount = 10
|
|
)
|
|
|
|
type Split struct {
|
|
SplitId int64
|
|
TransactionId int64
|
|
Status int64
|
|
ImportSplitType int64
|
|
|
|
// One of AccountId and SecurityId must be -1
|
|
// In normal splits, AccountId will be valid and SecurityId will be -1. The
|
|
// only case where this is reversed is for transactions that have been
|
|
// imported and not yet associated with an account.
|
|
AccountId int64
|
|
SecurityId int64
|
|
|
|
RemoteId string // unique ID from server, for detecting duplicates
|
|
Number string // Check or reference number
|
|
Memo string
|
|
Amount string // String representation of decimal, suitable for passing to big.Rat.SetString()
|
|
}
|
|
|
|
func GetBigAmount(amt string) (*big.Rat, error) {
|
|
var r big.Rat
|
|
_, success := r.SetString(amt)
|
|
if !success {
|
|
return nil, errors.New("Couldn't convert string amount to big.Rat via SetString()")
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
func (s *Split) GetAmount() (*big.Rat, error) {
|
|
return GetBigAmount(s.Amount)
|
|
}
|
|
|
|
func (s *Split) Valid() bool {
|
|
if (s.AccountId == -1) == (s.SecurityId == -1) {
|
|
return false
|
|
}
|
|
_, err := s.GetAmount()
|
|
return err == nil
|
|
}
|
|
|
|
func (s *Split) AlreadyImportedTx(transaction *gorp.Transaction) (bool, error) {
|
|
count, err := transaction.SelectInt("SELECT COUNT(*) from splits where RemoteId=? and AccountId=?", s.RemoteId, s.AccountId)
|
|
return count == 1, err
|
|
}
|
|
|
|
type Transaction struct {
|
|
TransactionId int64
|
|
UserId int64
|
|
Description string
|
|
Date time.Time
|
|
Splits []*Split `db:"-"`
|
|
}
|
|
|
|
type TransactionList struct {
|
|
Transactions *[]Transaction `json:"transactions"`
|
|
}
|
|
|
|
type AccountTransactionsList struct {
|
|
Account *Account
|
|
Transactions *[]Transaction
|
|
TotalTransactions int64
|
|
BeginningBalance string
|
|
EndingBalance string
|
|
}
|
|
|
|
func (t *Transaction) Write(w http.ResponseWriter) error {
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(t)
|
|
}
|
|
|
|
func (t *Transaction) Read(json_str string) error {
|
|
dec := json.NewDecoder(strings.NewReader(json_str))
|
|
return dec.Decode(t)
|
|
}
|
|
|
|
func (tl *TransactionList) Write(w http.ResponseWriter) error {
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(tl)
|
|
}
|
|
|
|
func (atl *AccountTransactionsList) Write(w http.ResponseWriter) error {
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(atl)
|
|
}
|
|
|
|
func (t *Transaction) Valid() bool {
|
|
for i := range t.Splits {
|
|
if !t.Splits[i].Valid() {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Return a map of security ID's to big.Rat's containing the amount that
|
|
// security is imbalanced by
|
|
func (t *Transaction) GetImbalancesTx(transaction *gorp.Transaction) (map[int64]big.Rat, error) {
|
|
sums := make(map[int64]big.Rat)
|
|
|
|
if !t.Valid() {
|
|
return nil, errors.New("Transaction invalid")
|
|
}
|
|
|
|
for i := range t.Splits {
|
|
securityid := t.Splits[i].SecurityId
|
|
if t.Splits[i].AccountId != -1 {
|
|
var err error
|
|
var account *Account
|
|
account, err = GetAccountTx(transaction, t.Splits[i].AccountId, t.UserId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
securityid = account.SecurityId
|
|
}
|
|
amount, _ := t.Splits[i].GetAmount()
|
|
sum := sums[securityid]
|
|
(&sum).Add(&sum, amount)
|
|
sums[securityid] = sum
|
|
}
|
|
return sums, nil
|
|
}
|
|
|
|
// Returns true if all securities contained in this transaction are balanced,
|
|
// false otherwise
|
|
func (t *Transaction) Balanced(transaction *gorp.Transaction) (bool, error) {
|
|
var zero big.Rat
|
|
|
|
sums, err := t.GetImbalancesTx(transaction)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, security_sum := range sums {
|
|
if security_sum.Cmp(&zero) != 0 {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func GetTransaction(db *DB, transactionid int64, userid int64) (*Transaction, error) {
|
|
var t Transaction
|
|
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = transaction.SelectOne(&t, "SELECT * from transactions where UserId=? AND TransactionId=?", userid, transactionid)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
_, err = transaction.Select(&t.Splits, "SELECT * from splits where TransactionId=?", transactionid)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &t, nil
|
|
}
|
|
|
|
func GetTransactions(db *DB, userid int64) (*[]Transaction, error) {
|
|
var transactions []Transaction
|
|
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = transaction.Select(&transactions, "SELECT * from transactions where UserId=?", userid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for i := range transactions {
|
|
_, err := transaction.Select(&transactions[i].Splits, "SELECT * from splits where TransactionId=?", transactions[i].TransactionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &transactions, nil
|
|
}
|
|
|
|
func incrementAccountVersions(transaction *gorp.Transaction, user *User, accountids []int64) error {
|
|
for i := range accountids {
|
|
account, err := GetAccountTx(transaction, accountids[i], user.UserId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
account.AccountVersion++
|
|
count, err := transaction.Update(account)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count != 1 {
|
|
return errors.New("Updated more than one account")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type AccountMissingError struct{}
|
|
|
|
func (ame AccountMissingError) Error() string {
|
|
return "Account missing"
|
|
}
|
|
|
|
func InsertTransactionTx(transaction *gorp.Transaction, t *Transaction, user *User) error {
|
|
// Map of any accounts with transaction splits being added
|
|
a_map := make(map[int64]bool)
|
|
for i := range t.Splits {
|
|
if t.Splits[i].AccountId != -1 {
|
|
existing, err := transaction.SelectInt("SELECT count(*) from accounts where AccountId=?", t.Splits[i].AccountId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existing != 1 {
|
|
return AccountMissingError{}
|
|
}
|
|
a_map[t.Splits[i].AccountId] = true
|
|
} else if t.Splits[i].SecurityId == -1 {
|
|
return AccountMissingError{}
|
|
}
|
|
}
|
|
|
|
//increment versions for all accounts
|
|
var a_ids []int64
|
|
for id := range a_map {
|
|
a_ids = append(a_ids, id)
|
|
}
|
|
// ensure at least one of the splits is associated with an actual account
|
|
if len(a_ids) < 1 {
|
|
return AccountMissingError{}
|
|
}
|
|
err := incrementAccountVersions(transaction, user, a_ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
t.UserId = user.UserId
|
|
err = transaction.Insert(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i := range t.Splits {
|
|
t.Splits[i].TransactionId = t.TransactionId
|
|
t.Splits[i].SplitId = -1
|
|
err = transaction.Insert(t.Splits[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func InsertTransaction(db *DB, t *Transaction, user *User) error {
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = InsertTransactionTx(transaction, t, user)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func UpdateTransactionTx(transaction *gorp.Transaction, t *Transaction, user *User) error {
|
|
var existing_splits []*Split
|
|
|
|
_, err := transaction.Select(&existing_splits, "SELECT * from splits where TransactionId=?", t.TransactionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Map of any accounts with transaction splits being added
|
|
a_map := make(map[int64]bool)
|
|
|
|
// Make a map with any existing splits for this transaction
|
|
s_map := make(map[int64]bool)
|
|
for i := range existing_splits {
|
|
s_map[existing_splits[i].SplitId] = true
|
|
}
|
|
|
|
// Insert splits, updating any pre-existing ones
|
|
for i := range t.Splits {
|
|
t.Splits[i].TransactionId = t.TransactionId
|
|
_, ok := s_map[t.Splits[i].SplitId]
|
|
if ok {
|
|
count, err := transaction.Update(t.Splits[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count != 1 {
|
|
return errors.New("Updated more than one transaction split")
|
|
}
|
|
delete(s_map, t.Splits[i].SplitId)
|
|
} else {
|
|
t.Splits[i].SplitId = -1
|
|
err := transaction.Insert(t.Splits[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if t.Splits[i].AccountId != -1 {
|
|
a_map[t.Splits[i].AccountId] = true
|
|
}
|
|
}
|
|
|
|
// Delete any remaining pre-existing splits
|
|
for i := range existing_splits {
|
|
_, ok := s_map[existing_splits[i].SplitId]
|
|
if existing_splits[i].AccountId != -1 {
|
|
a_map[existing_splits[i].AccountId] = true
|
|
}
|
|
if ok {
|
|
_, err := transaction.Delete(existing_splits[i])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Increment versions for all accounts with modified splits
|
|
var a_ids []int64
|
|
for id := range a_map {
|
|
a_ids = append(a_ids, id)
|
|
}
|
|
err = incrementAccountVersions(transaction, user, a_ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
count, err := transaction.Update(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count != 1 {
|
|
return errors.New("Updated more than one transaction")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func DeleteTransaction(db *DB, t *Transaction, user *User) error {
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var accountids []int64
|
|
_, err = transaction.Select(&accountids, "SELECT DISTINCT AccountId FROM splits WHERE TransactionId=? AND AccountId != -1", t.TransactionId)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
_, err = transaction.Exec("DELETE FROM splits WHERE TransactionId=?", t.TransactionId)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
count, err := transaction.Delete(t)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
if count != 1 {
|
|
transaction.Rollback()
|
|
return errors.New("Deleted more than one transaction")
|
|
}
|
|
|
|
err = incrementAccountVersions(transaction, user, accountids)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func TransactionHandler(w http.ResponseWriter, r *http.Request, db *DB) {
|
|
user, err := GetUserFromSession(db, r)
|
|
if err != nil {
|
|
WriteError(w, 1 /*Not Signed In*/)
|
|
return
|
|
}
|
|
|
|
if r.Method == "POST" {
|
|
transaction_json := r.PostFormValue("transaction")
|
|
if transaction_json == "" {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
var transaction Transaction
|
|
err := transaction.Read(transaction_json)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
transaction.TransactionId = -1
|
|
transaction.UserId = user.UserId
|
|
|
|
sqltx, err := db.Begin()
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
balanced, err := transaction.Balanced(sqltx)
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
if !transaction.Valid() || !balanced {
|
|
sqltx.Rollback()
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
for i := range transaction.Splits {
|
|
transaction.Splits[i].SplitId = -1
|
|
_, err := GetAccountTx(sqltx, transaction.Splits[i].AccountId, user.UserId)
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
}
|
|
|
|
err = InsertTransactionTx(sqltx, &transaction, user)
|
|
if err != nil {
|
|
if _, ok := err.(AccountMissingError); ok {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
} else {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
}
|
|
sqltx.Rollback()
|
|
return
|
|
}
|
|
|
|
err = sqltx.Commit()
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
err = transaction.Write(w)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
} else if r.Method == "GET" {
|
|
transactionid, err := GetURLID(r.URL.Path)
|
|
|
|
if err != nil {
|
|
//Return all Transactions
|
|
var al TransactionList
|
|
transactions, err := GetTransactions(db, user.UserId)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
al.Transactions = transactions
|
|
err = (&al).Write(w)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
} else {
|
|
//Return Transaction with this Id
|
|
transaction, err := GetTransaction(db, transactionid, user.UserId)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
err = transaction.Write(w)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
transactionid, err := GetURLID(r.URL.Path)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
if r.Method == "PUT" {
|
|
transaction_json := r.PostFormValue("transaction")
|
|
if transaction_json == "" {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
var transaction Transaction
|
|
err := transaction.Read(transaction_json)
|
|
if err != nil || transaction.TransactionId != transactionid {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
transaction.UserId = user.UserId
|
|
|
|
sqltx, err := db.Begin()
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
balanced, err := transaction.Balanced(sqltx)
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
if !transaction.Valid() || !balanced {
|
|
sqltx.Rollback()
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
for i := range transaction.Splits {
|
|
_, err := GetAccountTx(sqltx, transaction.Splits[i].AccountId, user.UserId)
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
}
|
|
|
|
err = UpdateTransactionTx(sqltx, &transaction, user)
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
err = sqltx.Commit()
|
|
if err != nil {
|
|
sqltx.Rollback()
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
err = transaction.Write(w)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
} else if r.Method == "DELETE" {
|
|
transactionid, err := GetURLID(r.URL.Path)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
transaction, err := GetTransaction(db, transactionid, user.UserId)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
|
|
err = DeleteTransaction(db, transaction, user)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
WriteSuccess(w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TransactionsBalanceDifference(transaction *gorp.Transaction, accountid int64, transactions []Transaction) (*big.Rat, error) {
|
|
var pageDifference, tmp big.Rat
|
|
for i := range transactions {
|
|
_, err := transaction.Select(&transactions[i].Splits, "SELECT * FROM splits where TransactionId=?", transactions[i].TransactionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Sum up the amounts from the splits we're returning so we can return
|
|
// an ending balance
|
|
for j := range transactions[i].Splits {
|
|
if transactions[i].Splits[j].AccountId == accountid {
|
|
rat_amount, err := GetBigAmount(transactions[i].Splits[j].Amount)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tmp.Add(&pageDifference, rat_amount)
|
|
pageDifference.Set(&tmp)
|
|
}
|
|
}
|
|
}
|
|
return &pageDifference, nil
|
|
}
|
|
|
|
func GetAccountBalance(db *DB, user *User, accountid int64) (*big.Rat, error) {
|
|
var splits []Split
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sql := "SELECT DISTINCT splits.* FROM splits INNER JOIN transactions ON transactions.TransactionId = splits.TransactionId WHERE splits.AccountId=? AND transactions.UserId=?"
|
|
_, err = transaction.Select(&splits, sql, accountid, user.UserId)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
var balance, tmp big.Rat
|
|
for _, s := range splits {
|
|
rat_amount, err := GetBigAmount(s.Amount)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
tmp.Add(&balance, rat_amount)
|
|
balance.Set(&tmp)
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &balance, nil
|
|
}
|
|
|
|
// Assumes accountid is valid and is owned by the current user
|
|
func GetAccountBalanceDate(db *DB, user *User, accountid int64, date *time.Time) (*big.Rat, error) {
|
|
var splits []Split
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sql := "SELECT DISTINCT splits.* FROM splits INNER JOIN transactions ON transactions.TransactionId = splits.TransactionId WHERE splits.AccountId=? AND transactions.UserId=? AND transactions.Date < ?"
|
|
_, err = transaction.Select(&splits, sql, accountid, user.UserId, date)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
var balance, tmp big.Rat
|
|
for _, s := range splits {
|
|
rat_amount, err := GetBigAmount(s.Amount)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
tmp.Add(&balance, rat_amount)
|
|
balance.Set(&tmp)
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &balance, nil
|
|
}
|
|
|
|
func GetAccountBalanceDateRange(db *DB, user *User, accountid int64, begin, end *time.Time) (*big.Rat, error) {
|
|
var splits []Split
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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 = transaction.Select(&splits, sql, accountid, user.UserId, begin, end)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
var balance, tmp big.Rat
|
|
for _, s := range splits {
|
|
rat_amount, err := GetBigAmount(s.Amount)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
tmp.Add(&balance, rat_amount)
|
|
balance.Set(&tmp)
|
|
}
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &balance, nil
|
|
}
|
|
|
|
func GetAccountTransactions(db *DB, user *User, accountid int64, sort string, page uint64, limit uint64) (*AccountTransactionsList, error) {
|
|
var transactions []Transaction
|
|
var atl AccountTransactionsList
|
|
|
|
transaction, err := db.Begin()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var sqlsort, balanceLimitOffset string
|
|
var balanceLimitOffsetArg uint64
|
|
if sort == "date-asc" {
|
|
sqlsort = " ORDER BY transactions.Date ASC"
|
|
balanceLimitOffset = " LIMIT ?"
|
|
balanceLimitOffsetArg = page * limit
|
|
} else if sort == "date-desc" {
|
|
numSplits, err := transaction.SelectInt("SELECT count(*) FROM splits")
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
sqlsort = " ORDER BY transactions.Date DESC"
|
|
balanceLimitOffset = fmt.Sprintf(" LIMIT %d OFFSET ?", numSplits)
|
|
balanceLimitOffsetArg = (page + 1) * limit
|
|
}
|
|
|
|
var sqloffset string
|
|
if page > 0 {
|
|
sqloffset = fmt.Sprintf(" OFFSET %d", page*limit)
|
|
}
|
|
|
|
account, err := GetAccountTx(transaction, accountid, user.UserId)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
atl.Account = account
|
|
|
|
sql := "SELECT DISTINCT transactions.* FROM transactions INNER JOIN splits ON transactions.TransactionId = splits.TransactionId WHERE transactions.UserId=? AND splits.AccountId=?" + sqlsort + " LIMIT ?" + sqloffset
|
|
_, err = transaction.Select(&transactions, sql, user.UserId, accountid, limit)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
atl.Transactions = &transactions
|
|
|
|
pageDifference, err := TransactionsBalanceDifference(transaction, accountid, transactions)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
count, err := transaction.SelectInt("SELECT count(DISTINCT transactions.TransactionId) FROM transactions INNER JOIN splits ON transactions.TransactionId = splits.TransactionId WHERE transactions.UserId=? AND splits.AccountId=?", user.UserId, accountid)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
atl.TotalTransactions = count
|
|
|
|
security, err := GetSecurityTx(transaction, atl.Account.SecurityId, user.UserId)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
if security == nil {
|
|
transaction.Rollback()
|
|
return nil, errors.New("Security not found")
|
|
}
|
|
|
|
// Sum all the splits for all transaction splits for this account that
|
|
// occurred before the page we're returning
|
|
var amounts []string
|
|
sql = "SELECT splits.Amount FROM splits WHERE splits.AccountId=? AND splits.TransactionId IN (SELECT DISTINCT transactions.TransactionId FROM transactions INNER JOIN splits ON transactions.TransactionId = splits.TransactionId WHERE transactions.UserId=? AND splits.AccountId=?" + sqlsort + balanceLimitOffset + ")"
|
|
_, err = transaction.Select(&amounts, sql, accountid, user.UserId, accountid, balanceLimitOffsetArg)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
var tmp, balance big.Rat
|
|
for _, amount := range amounts {
|
|
rat_amount, err := GetBigAmount(amount)
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
tmp.Add(&balance, rat_amount)
|
|
balance.Set(&tmp)
|
|
}
|
|
atl.BeginningBalance = balance.FloatString(security.Precision)
|
|
atl.EndingBalance = tmp.Add(&balance, pageDifference).FloatString(security.Precision)
|
|
|
|
err = transaction.Commit()
|
|
if err != nil {
|
|
transaction.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
return &atl, nil
|
|
}
|
|
|
|
// Return only those transactions which have at least one split pertaining to
|
|
// an account
|
|
func AccountTransactionsHandler(db *DB, w http.ResponseWriter, r *http.Request,
|
|
user *User, accountid int64) {
|
|
|
|
var page uint64 = 0
|
|
var limit uint64 = 50
|
|
var sort string = "date-desc"
|
|
|
|
query, _ := url.ParseQuery(r.URL.RawQuery)
|
|
|
|
pagestring := query.Get("page")
|
|
if pagestring != "" {
|
|
p, err := strconv.ParseUint(pagestring, 10, 0)
|
|
if err != nil {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
page = p
|
|
}
|
|
|
|
limitstring := query.Get("limit")
|
|
if limitstring != "" {
|
|
l, err := strconv.ParseUint(limitstring, 10, 0)
|
|
if err != nil || l > 100 {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
limit = l
|
|
}
|
|
|
|
sortstring := query.Get("sort")
|
|
if sortstring != "" {
|
|
if sortstring != "date-asc" && sortstring != "date-desc" {
|
|
WriteError(w, 3 /*Invalid Request*/)
|
|
return
|
|
}
|
|
sort = sortstring
|
|
}
|
|
|
|
accountTransactions, err := GetAccountTransactions(db, user, accountid, sort, page, limit)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
|
|
err = accountTransactions.Write(w)
|
|
if err != nil {
|
|
WriteError(w, 999 /*Internal Error*/)
|
|
log.Print(err)
|
|
return
|
|
}
|
|
}
|