mirror of
https://github.com/aclindsa/moneygo.git
synced 2024-11-17 03:50:05 -05:00
Aaron Lindsay
a357d38eee
This adds 'shadow' types used only by the store/db internal package whch handle converting these types to their DB-equivalent values. This change should allow reports to be generated significantly faster since it allows a large portion of the computation to be shifted to the database engines.
481 lines
12 KiB
Go
481 lines
12 KiB
Go
package integration_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/aclindsa/moneygo/internal/models"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Needed because handlers.User doesn't allow Password to be written to JSON
|
|
type User struct {
|
|
UserId int64
|
|
DefaultCurrency int64 // SecurityId of default currency, or ISO4217 code for it if creating new user
|
|
Name string
|
|
Username string
|
|
Password string
|
|
PasswordHash string
|
|
Email string
|
|
}
|
|
|
|
func (u *User) Write(w http.ResponseWriter) error {
|
|
enc := json.NewEncoder(w)
|
|
return enc.Encode(u)
|
|
}
|
|
|
|
func (u *User) Read(json_str string) error {
|
|
dec := json.NewDecoder(strings.NewReader(json_str))
|
|
return dec.Decode(u)
|
|
}
|
|
|
|
// TestData
|
|
type TestData struct {
|
|
initialized bool
|
|
users []User
|
|
clients []*http.Client
|
|
securities []models.Security
|
|
prices []models.Price
|
|
accounts []models.Account // accounts must appear after their parents in this slice
|
|
transactions []models.Transaction
|
|
reports []models.Report
|
|
tabulations []models.Tabulation
|
|
}
|
|
|
|
type TestDataFunc func(*testing.T, *TestData)
|
|
|
|
func (t *TestData) initUser(user *User, userid int) error {
|
|
newuser, err := createUser(user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
t.users = append(t.users, *newuser)
|
|
|
|
// make a copy of the user so we can set the password for creating the
|
|
// session without disturbing the original
|
|
userWithPassword := *newuser
|
|
userWithPassword.Password = user.Password
|
|
|
|
client, err := newSession(&userWithPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
t.clients = append(t.clients, client)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Initialize makes requests to the server to create all of the objects
|
|
// represented in it before returning a copy of the data, with all of the *Id
|
|
// fields updated to their actual values
|
|
func (t *TestData) Initialize() (*TestData, error) {
|
|
var t2 TestData
|
|
for userid, user := range t.users {
|
|
err := t2.initUser(&user, userid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
for _, security := range t.securities {
|
|
s2, err := createSecurity(t2.clients[security.UserId], &security)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t2.securities = append(t2.securities, *s2)
|
|
}
|
|
|
|
for _, price := range t.prices {
|
|
userid := t.securities[price.SecurityId].UserId
|
|
price.SecurityId = t2.securities[price.SecurityId].SecurityId
|
|
price.CurrencyId = t2.securities[price.CurrencyId].SecurityId
|
|
p2, err := createPrice(t2.clients[userid], &price)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t2.prices = append(t2.prices, *p2)
|
|
}
|
|
|
|
for _, account := range t.accounts {
|
|
account.SecurityId = t2.securities[account.SecurityId].SecurityId
|
|
if account.ParentAccountId != -1 {
|
|
account.ParentAccountId = t2.accounts[account.ParentAccountId].AccountId
|
|
}
|
|
a2, err := createAccount(t2.clients[account.UserId], &account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t2.accounts = append(t2.accounts, *a2)
|
|
}
|
|
|
|
for i, transaction := range t.transactions {
|
|
transaction.Splits = []*models.Split{}
|
|
for _, s := range t.transactions[i].Splits {
|
|
// Make a copy of the split since Splits is a slice of pointers so
|
|
// copying the transaction doesn't
|
|
split := *s
|
|
split.AccountId = t2.accounts[split.AccountId].AccountId
|
|
transaction.Splits = append(transaction.Splits, &split)
|
|
}
|
|
tt2, err := createTransaction(t2.clients[transaction.UserId], &transaction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t2.transactions = append(t2.transactions, *tt2)
|
|
}
|
|
|
|
for _, report := range t.reports {
|
|
r2, err := createReport(t2.clients[report.UserId], &report)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t2.reports = append(t2.reports, *r2)
|
|
}
|
|
|
|
t2.initialized = true
|
|
return &t2, nil
|
|
}
|
|
|
|
func (t *TestData) Teardown() error {
|
|
if !t.initialized {
|
|
return fmt.Errorf("Cannot teardown uninitialized TestData")
|
|
}
|
|
for userid, user := range t.users {
|
|
err := deleteUser(t.clients[userid], &user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var data = []TestData{
|
|
{
|
|
users: []User{
|
|
{
|
|
DefaultCurrency: 840, // USD
|
|
Name: "John Smith",
|
|
Username: "jsmith",
|
|
Password: "hunter2",
|
|
Email: "jsmith@example.com",
|
|
},
|
|
{
|
|
DefaultCurrency: 978, // Euro
|
|
Name: "Billy Bob",
|
|
Username: "bbob6",
|
|
Password: "#)$&!KF(*ADAHK#@*(FAJSDkalsdf98af32klhf98sd8a'2938LKJD",
|
|
Email: "bbob+moneygo@my-domain.com",
|
|
},
|
|
},
|
|
securities: []models.Security{
|
|
{
|
|
UserId: 0,
|
|
Name: "USD",
|
|
Description: "US Dollar",
|
|
Symbol: "$",
|
|
Precision: 2,
|
|
Type: models.Currency,
|
|
AlternateId: "840",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
Name: "SPY",
|
|
Description: "SPDR S&P 500 ETF Trust",
|
|
Symbol: "SPY",
|
|
Precision: 5,
|
|
Type: models.Stock,
|
|
AlternateId: "78462F103",
|
|
},
|
|
{
|
|
UserId: 1,
|
|
Name: "EUR",
|
|
Description: "Euro",
|
|
Symbol: "€",
|
|
Precision: 2,
|
|
Type: models.Currency,
|
|
AlternateId: "978",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
Name: "EUR",
|
|
Description: "Euro",
|
|
Symbol: "€",
|
|
Precision: 2,
|
|
Type: models.Currency,
|
|
AlternateId: "978",
|
|
},
|
|
},
|
|
prices: []models.Price{
|
|
{
|
|
SecurityId: 1,
|
|
CurrencyId: 0,
|
|
Date: time.Date(2017, time.January, 2, 21, 0, 0, 0, time.UTC),
|
|
Value: NewAmount("225.24"),
|
|
RemoteId: "12387-129831-1238",
|
|
},
|
|
{
|
|
SecurityId: 1,
|
|
CurrencyId: 0,
|
|
Date: time.Date(2017, time.January, 3, 21, 0, 0, 0, time.UTC),
|
|
Value: NewAmount("226.58"),
|
|
RemoteId: "12387-129831-1239",
|
|
},
|
|
{
|
|
SecurityId: 1,
|
|
CurrencyId: 0,
|
|
Date: time.Date(2017, time.January, 4, 21, 0, 0, 0, time.UTC),
|
|
Value: NewAmount("226.40"),
|
|
RemoteId: "12387-129831-1240",
|
|
},
|
|
{
|
|
SecurityId: 1,
|
|
CurrencyId: 0,
|
|
Date: time.Date(2017, time.January, 5, 21, 0, 0, 0, time.UTC),
|
|
Value: NewAmount("227.21"),
|
|
RemoteId: "12387-129831-1241",
|
|
},
|
|
{
|
|
SecurityId: 0,
|
|
CurrencyId: 3,
|
|
Date: time.Date(2017, time.November, 16, 18, 49, 53, 0, time.UTC),
|
|
Value: NewAmount("0.85"),
|
|
RemoteId: "USDEUR819298714",
|
|
},
|
|
},
|
|
accounts: []models.Account{
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: -1,
|
|
Type: models.Asset,
|
|
Name: "Assets",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: 0,
|
|
Type: models.Bank,
|
|
Name: "Credit Union Checking",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: -1,
|
|
Type: models.Expense,
|
|
Name: "Expenses",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: 2,
|
|
Type: models.Expense,
|
|
Name: "Groceries",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: 2,
|
|
Type: models.Expense,
|
|
Name: "Cable",
|
|
},
|
|
{
|
|
UserId: 1,
|
|
SecurityId: 2,
|
|
ParentAccountId: -1,
|
|
Type: models.Asset,
|
|
Name: "Assets",
|
|
},
|
|
{
|
|
UserId: 1,
|
|
SecurityId: 2,
|
|
ParentAccountId: -1,
|
|
Type: models.Expense,
|
|
Name: "Expenses",
|
|
},
|
|
{
|
|
UserId: 0,
|
|
SecurityId: 0,
|
|
ParentAccountId: -1,
|
|
Type: models.Liability,
|
|
Name: "Credit Card",
|
|
},
|
|
},
|
|
transactions: []models.Transaction{
|
|
{
|
|
UserId: 0,
|
|
Description: "weekly groceries",
|
|
Date: time.Date(2017, time.October, 15, 1, 16, 59, 0, time.UTC),
|
|
Splits: []*models.Split{
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 1,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("-5.6"),
|
|
},
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 3,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("5.6"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserId: 0,
|
|
Description: "weekly groceries",
|
|
Date: time.Date(2017, time.October, 31, 19, 10, 14, 0, time.UTC),
|
|
Splits: []*models.Split{
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 1,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("-81.59"),
|
|
},
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 3,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("81.59"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserId: 0,
|
|
Description: "Cable",
|
|
Date: time.Date(2017, time.September, 2, 0, 00, 00, 0, time.UTC),
|
|
Splits: []*models.Split{
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 1,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("-39.99"),
|
|
},
|
|
{
|
|
Status: models.Entered,
|
|
AccountId: 4,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("39.99"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserId: 1,
|
|
Description: "Gas",
|
|
Date: time.Date(2017, time.November, 1, 13, 19, 50, 0, time.UTC),
|
|
Splits: []*models.Split{
|
|
{
|
|
Status: models.Reconciled,
|
|
AccountId: 5,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("-24.56"),
|
|
},
|
|
{
|
|
Status: models.Entered,
|
|
AccountId: 6,
|
|
SecurityId: -1,
|
|
Amount: NewAmount("24.56"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
reports: []models.Report{
|
|
{
|
|
UserId: 0,
|
|
Name: "This Year's Monthly Expenses",
|
|
Lua: `
|
|
function account_series_map(accounts, tabulation)
|
|
map = {}
|
|
|
|
for i=1,100 do -- we're not messing with accounts more than 100 levels deep
|
|
all_handled = true
|
|
for id, acct in pairs(accounts) do
|
|
if not map[id] then
|
|
all_handled = false
|
|
if not acct.parent then
|
|
map[id] = tabulation:series(acct.name)
|
|
elseif map[acct.parent.accountid] then
|
|
map[id] = map[acct.parent.accountid]:series(acct.name)
|
|
end
|
|
end
|
|
end
|
|
if all_handled then
|
|
return map
|
|
end
|
|
end
|
|
|
|
error("Accounts nested (at least) 100 levels deep")
|
|
end
|
|
|
|
function generate()
|
|
year = 2017
|
|
account_type = account.Expense
|
|
|
|
accounts = get_accounts()
|
|
t = tabulation.new(12)
|
|
t:title(year .. " Monthly Expenses")
|
|
t:subtitle("This is my subtitle")
|
|
t:units(get_default_currency().Symbol)
|
|
series_map = account_series_map(accounts, t)
|
|
|
|
for month=1,12 do
|
|
begin_date = date.new(year, month, 1)
|
|
end_date = date.new(year, month+1, 1)
|
|
|
|
t:label(month, tostring(begin_date))
|
|
|
|
for id, acct in pairs(accounts) do
|
|
series = series_map[id]
|
|
if acct.type == account_type then
|
|
balance = acct:balance(begin_date, end_date)
|
|
series:value(month, balance.amount)
|
|
end
|
|
end
|
|
end
|
|
|
|
return t
|
|
end`,
|
|
},
|
|
},
|
|
tabulations: []models.Tabulation{
|
|
{
|
|
ReportId: 0,
|
|
Title: "2017 Monthly Expenses",
|
|
Subtitle: "This is my subtitle",
|
|
Units: "USD",
|
|
Labels: []string{"2017-01-01", "2017-02-01", "2017-03-01", "2017-04-01", "2017-05-01", "2017-06-01", "2017-07-01", "2017-08-01", "2017-09-01", "2017-10-01", "2017-11-01", "2017-12-01"},
|
|
Series: map[string]*models.Series{
|
|
"Assets": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
Series: map[string]*models.Series{
|
|
"Credit Union Checking": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
Series: map[string]*models.Series{},
|
|
},
|
|
},
|
|
},
|
|
"Expenses": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
Series: map[string]*models.Series{
|
|
"Groceries": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 87.19, 0, 0},
|
|
Series: map[string]*models.Series{},
|
|
},
|
|
"Cable": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 39.99, 0, 0, 0},
|
|
Series: map[string]*models.Series{},
|
|
},
|
|
},
|
|
},
|
|
"Credit Card": {
|
|
Values: []float64{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
|
Series: map[string]*models.Series{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|