2015-06-25 22:36:58 -04:00
package main
import (
2015-06-28 23:03:34 -04:00
"encoding/json"
"errors"
2015-07-11 08:58:36 -04:00
"fmt"
"gopkg.in/gorp.v1"
2015-06-28 23:03:34 -04:00
"log"
2015-06-25 22:36:58 -04:00
"math/big"
2015-06-28 23:03:34 -04:00
"net/http"
2015-07-11 08:58:36 -04:00
"net/url"
"strconv"
2015-06-28 23:03:34 -04:00
"strings"
2015-06-25 22:36:58 -04:00
"time"
)
type Split struct {
SplitId int64
TransactionId int64
AccountId int64
2015-08-05 21:25:25 -04:00
Number string // Check or reference number
2015-06-25 22:36:58 -04:00
Memo string
2015-06-28 23:03:34 -04:00
Amount string // String representation of decimal, suitable for passing to big.Rat.SetString()
2015-06-25 22:36:58 -04:00
}
2015-08-21 06:54:17 -04:00
func GetBigAmount ( amt string ) ( * big . Rat , error ) {
2015-06-28 23:03:34 -04:00
var r big . Rat
2015-08-21 06:54:17 -04:00
_ , success := r . SetString ( amt )
2015-06-28 23:03:34 -04:00
if ! success {
2015-08-21 06:54:17 -04:00
return nil , errors . New ( "Couldn't convert string amount to big.Rat via SetString()" )
2015-06-28 23:03:34 -04:00
}
return & r , nil
}
2015-08-21 06:54:17 -04:00
func ( s * Split ) GetAmount ( ) ( * big . Rat , error ) {
return GetBigAmount ( s . Amount )
}
2015-06-28 23:03:34 -04:00
func ( s * Split ) Valid ( ) bool {
_ , err := s . GetAmount ( )
return err == nil
}
2015-06-25 22:36:58 -04:00
const (
2015-08-05 21:25:25 -04:00
Entered int64 = 1
2015-08-08 09:08:37 -04:00
Cleared = 2
Reconciled = 3
Voided = 4
2015-06-25 22:36:58 -04:00
)
type Transaction struct {
TransactionId int64
UserId int64
Description string
2015-08-05 21:25:25 -04:00
Status int64
2015-06-25 22:36:58 -04:00
Date time . Time
2015-06-28 23:03:34 -04:00
Splits [ ] * Split ` db:"-" `
}
type TransactionList struct {
Transactions * [ ] Transaction ` json:"transactions" `
}
2015-07-11 08:58:36 -04:00
type AccountTransactionsList struct {
2015-08-21 06:54:17 -04:00
Account * Account
Transactions * [ ] Transaction
TotalTransactions int64
BeginningBalance string
EndingBalance string
2015-07-11 08:58:36 -04:00
}
2015-06-28 23:03:34 -04:00
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 )
}
2015-07-11 08:58:36 -04:00
func ( atl * AccountTransactionsList ) Write ( w http . ResponseWriter ) error {
enc := json . NewEncoder ( w )
return enc . Encode ( atl )
}
2015-06-28 23:03:34 -04:00
func ( t * Transaction ) Valid ( ) bool {
for i := range t . Splits {
if ! t . Splits [ i ] . Valid ( ) {
return false
}
}
return true
}
func ( t * Transaction ) Balanced ( ) bool {
var zero , sum big . Rat
if ! t . Valid ( ) {
return false // TODO Open question: should we report an error here instead?
}
for i := range t . Splits {
amount , _ := t . Splits [ i ] . GetAmount ( )
if t . Splits [ i ] . Debit {
sum . Add ( & sum , amount )
} else {
sum . Sub ( & sum , amount )
}
}
return sum . Cmp ( & zero ) == 0
}
func GetTransaction ( transactionid int64 , userid int64 ) ( * Transaction , error ) {
var t Transaction
transaction , err := DB . Begin ( )
if err != nil {
return nil , err
}
2015-08-05 21:25:25 -04:00
err = transaction . SelectOne ( & t , "SELECT * from transactions where UserId=? AND TransactionId=?" , userid , transactionid )
2015-06-28 23:03:34 -04:00
if err != nil {
return nil , err
}
_ , err = transaction . Select ( & t . Splits , "SELECT * from splits where TransactionId=?" , transactionid )
if err != nil {
return nil , err
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return nil , err
}
return & t , nil
}
func GetTransactions ( 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
}
2015-07-11 08:58:36 -04:00
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
}
2015-08-05 21:25:25 -04:00
account . AccountVersion ++
2015-07-11 08:58:36 -04:00
count , err := transaction . Update ( account )
if err != nil {
return err
}
if count != 1 {
return errors . New ( "Updated more than one account" )
}
}
return nil
}
2015-06-28 23:03:34 -04:00
type AccountMissingError struct { }
func ( ame AccountMissingError ) Error ( ) string {
return "Account missing"
}
2015-07-11 08:58:36 -04:00
func InsertTransaction ( t * Transaction , user * User ) error {
2015-06-28 23:03:34 -04:00
transaction , err := DB . Begin ( )
if err != nil {
return err
}
2015-07-11 08:58:36 -04:00
// Map of any accounts with transaction splits being added
a_map := make ( map [ int64 ] bool )
2015-06-28 23:03:34 -04:00
for i := range t . Splits {
existing , err := transaction . SelectInt ( "SELECT count(*) from accounts where AccountId=?" , t . Splits [ i ] . AccountId )
if err != nil {
transaction . Rollback ( )
return err
}
if existing != 1 {
transaction . Rollback ( )
return AccountMissingError { }
}
2015-07-11 08:58:36 -04:00
a_map [ t . Splits [ i ] . AccountId ] = true
}
//increment versions for all accounts
var a_ids [ ] int64
for id := range a_map {
a_ids = append ( a_ids , id )
}
err = incrementAccountVersions ( transaction , user , a_ids )
if err != nil {
transaction . Rollback ( )
return err
2015-06-28 23:03:34 -04:00
}
err = transaction . Insert ( t )
if err != nil {
transaction . Rollback ( )
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 {
transaction . Rollback ( )
return err
}
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return err
}
return nil
}
2015-07-11 08:58:36 -04:00
func UpdateTransaction ( t * Transaction , user * User ) error {
2015-06-28 23:03:34 -04:00
transaction , err := DB . Begin ( )
if err != nil {
return err
}
var existing_splits [ ] * Split
_ , err = transaction . Select ( & existing_splits , "SELECT * from splits where TransactionId=?" , t . TransactionId )
if err != nil {
transaction . Rollback ( )
return err
}
2015-07-11 08:58:36 -04:00
// Map of any accounts with transaction splits being added
a_map := make ( map [ int64 ] bool )
2015-06-28 23:03:34 -04:00
// Make a map with any existing splits for this transaction
2015-07-11 08:58:36 -04:00
s_map := make ( map [ int64 ] bool )
2015-06-28 23:03:34 -04:00
for i := range existing_splits {
2015-07-11 08:58:36 -04:00
s_map [ existing_splits [ i ] . SplitId ] = true
2015-06-28 23:03:34 -04:00
}
// Insert splits, updating any pre-existing ones
for i := range t . Splits {
t . Splits [ i ] . TransactionId = t . TransactionId
2015-07-11 08:58:36 -04:00
_ , ok := s_map [ t . Splits [ i ] . SplitId ]
2015-06-28 23:03:34 -04:00
if ok {
count , err := transaction . Update ( t . Splits [ i ] )
if err != nil {
transaction . Rollback ( )
return err
}
if count != 1 {
transaction . Rollback ( )
return errors . New ( "Updated more than one transaction split" )
}
} else {
t . Splits [ i ] . SplitId = - 1
err := transaction . Insert ( t . Splits [ i ] )
if err != nil {
transaction . Rollback ( )
return err
}
}
2015-07-11 08:58:36 -04:00
a_map [ t . Splits [ i ] . AccountId ] = true
2015-06-28 23:03:34 -04:00
}
// Delete any remaining pre-existing splits
for i := range existing_splits {
2015-07-11 08:58:36 -04:00
_ , ok := s_map [ existing_splits [ i ] . SplitId ]
a_map [ existing_splits [ i ] . AccountId ] = true
2015-06-28 23:03:34 -04:00
if ok {
2015-07-11 08:58:36 -04:00
_ , err := transaction . Delete ( existing_splits [ i ] )
2015-06-28 23:03:34 -04:00
if err != nil {
transaction . Rollback ( )
return err
}
}
}
2015-07-11 08:58:36 -04:00
// 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 {
transaction . Rollback ( )
return err
}
2015-06-28 23:03:34 -04:00
count , err := transaction . Update ( t )
if err != nil {
transaction . Rollback ( )
return err
}
if count != 1 {
transaction . Rollback ( )
return errors . New ( "Updated more than one transaction" )
}
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return err
}
return nil
}
2015-07-11 08:58:36 -04:00
func DeleteTransaction ( t * Transaction , user * User ) error {
2015-06-29 07:25:29 -04:00
transaction , err := DB . Begin ( )
if err != nil {
return err
}
2015-07-11 08:58:36 -04:00
var accountids [ ] int64
_ , err = transaction . Select ( & accountids , "SELECT DISTINCT AccountId FROM splits WHERE TransactionId=?" , t . TransactionId )
if err != nil {
transaction . Rollback ( )
return err
}
_ , err = transaction . Exec ( "DELETE FROM splits WHERE TransactionId=?" , t . TransactionId )
2015-06-29 07:25:29 -04:00
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" )
}
2015-07-11 08:58:36 -04:00
err = incrementAccountVersions ( transaction , user , accountids )
if err != nil {
transaction . Rollback ( )
return err
}
2015-06-29 07:25:29 -04:00
err = transaction . Commit ( )
if err != nil {
transaction . Rollback ( )
return err
}
return nil
}
2015-06-28 23:03:34 -04:00
func TransactionHandler ( w http . ResponseWriter , r * http . Request ) {
user , err := GetUserFromSession ( 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
if ! transaction . Valid ( ) || ! transaction . Balanced ( ) {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
for i := range transaction . Splits {
transaction . Splits [ i ] . SplitId = - 1
_ , err := GetAccount ( transaction . Splits [ i ] . AccountId , user . UserId )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
}
2015-07-11 08:58:36 -04:00
err = InsertTransaction ( & transaction , user )
2015-06-28 23:03:34 -04:00
if err != nil {
if _ , ok := err . ( AccountMissingError ) ; ok {
WriteError ( w , 3 /*Invalid Request*/ )
} else {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
}
return
}
WriteSuccess ( w )
} else if r . Method == "GET" {
transactionid , err := GetURLID ( r . URL . Path )
2015-07-11 08:58:36 -04:00
2015-06-28 23:03:34 -04:00
if err != nil {
//Return all Transactions
var al TransactionList
transactions , err := GetTransactions ( 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 ( 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
if ! transaction . Valid ( ) || ! transaction . Balanced ( ) {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
for i := range transaction . Splits {
transaction . Splits [ i ] . SplitId = - 1
_ , err := GetAccount ( transaction . Splits [ i ] . AccountId , user . UserId )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
}
2015-07-11 08:58:36 -04:00
err = UpdateTransaction ( & transaction , user )
2015-06-28 23:03:34 -04:00
if err != nil {
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
WriteSuccess ( w )
} else if r . Method == "DELETE" {
transactionid , err := GetURLID ( r . URL . Path )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
transaction , err := GetTransaction ( transactionid , user . UserId )
if err != nil {
WriteError ( w , 3 /*Invalid Request*/ )
return
}
2015-07-11 08:58:36 -04:00
err = DeleteTransaction ( transaction , user )
2015-06-29 07:25:29 -04:00
if err != nil {
2015-06-28 23:03:34 -04:00
WriteError ( w , 999 /*Internal Error*/ )
log . Print ( err )
return
}
WriteSuccess ( w )
}
}
2015-06-25 22:36:58 -04:00
}
2015-07-11 08:58:36 -04:00
func GetAccountTransactions ( user * User , accountid int64 , sort string , page uint64 , limit uint64 ) ( * AccountTransactionsList , error ) {
var transactions [ ] Transaction
var atl AccountTransactionsList
2015-08-22 09:46:11 -04:00
transaction , err := DB . Begin ( )
if err != nil {
return nil , err
}
var sqlsort , balanceLimitOffset string
var balanceLimitOffsetArg uint64
2015-07-11 08:58:36 -04:00
if sort == "date-asc" {
sqlsort = " ORDER BY transactions.Date ASC"
2015-08-22 09:46:11 -04:00
balanceLimitOffset = " LIMIT ?"
2015-08-30 19:43:26 -04:00
balanceLimitOffsetArg = page * limit
2015-07-11 08:58:36 -04:00
} else if sort == "date-desc" {
2015-08-22 09:46:11 -04:00
numSplits , err := transaction . SelectInt ( "SELECT count(*) FROM splits" )
if err != nil {
transaction . Rollback ( )
return nil , err
}
2015-07-11 08:58:36 -04:00
sqlsort = " ORDER BY transactions.Date DESC"
2015-08-22 09:46:11 -04:00
balanceLimitOffset = fmt . Sprintf ( " LIMIT %d OFFSET ?" , numSplits )
2015-08-30 19:43:26 -04:00
balanceLimitOffsetArg = ( page + 1 ) * limit
2015-07-11 08:58:36 -04:00
}
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
2015-08-08 09:05:36 -04:00
sql := "SELECT DISTINCT transactions.* FROM transactions INNER JOIN splits ON transactions.TransactionId = splits.TransactionId WHERE transactions.UserId=? AND splits.AccountId=?" + sqlsort + " LIMIT ?" + sqloffset
2015-07-11 08:58:36 -04:00
_ , err = transaction . Select ( & transactions , sql , user . UserId , accountid , limit )
if err != nil {
transaction . Rollback ( )
return nil , err
}
atl . Transactions = & transactions
2015-08-21 06:54:17 -04:00
var pageDifference , tmp big . Rat
2015-07-11 08:58:36 -04:00
for i := range transactions {
2015-08-08 09:05:36 -04:00
_ , err = transaction . Select ( & transactions [ i ] . Splits , "SELECT * FROM splits where TransactionId=?" , transactions [ i ] . TransactionId )
2015-07-11 08:58:36 -04:00
if err != nil {
transaction . Rollback ( )
return nil , err
}
2015-08-21 06:54:17 -04:00
// 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 {
transaction . Rollback ( )
return nil , err
}
tmp . Add ( & pageDifference , rat_amount )
pageDifference . Set ( & tmp )
}
}
2015-07-11 08:58:36 -04:00
}
2015-08-08 09:08:37 -04:00
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
2015-08-21 06:54:17 -04:00
security := GetSecurity ( atl . Account . SecurityId )
if security == nil {
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
2015-08-22 09:46:11 -04:00
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 )
2015-08-21 06:54:17 -04:00
if err != nil {
transaction . Rollback ( )
return nil , err
}
var 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 )
2015-07-11 08:58:36 -04:00
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 ( 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 ( 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
}
}