Compare commits
11 Commits
7f7df35fae
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
fff24d5686
|
|||
|
85721d7815
|
|||
| fc228d329e | |||
| 5a39ade237 | |||
| 1219ccf0cd | |||
| 520f3e312f | |||
| c7fd50b347 | |||
| 3dade3e3b0 | |||
| 8f93770e8d | |||
| 8a0ad44041 | |||
| 746b7ec657 |
50
example.yaml
Normal file
50
example.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
end: 2025-05-01
|
||||||
|
accounts:
|
||||||
|
- name: Capital One
|
||||||
|
balance: 4500
|
||||||
|
minimum: 500
|
||||||
|
credit:
|
||||||
|
- name: Amazon Store Card
|
||||||
|
amount: -438.35
|
||||||
|
date: 2023-03-28
|
||||||
|
- name: Amex Platinum
|
||||||
|
amount: -3263.42
|
||||||
|
date: 2023-04-19
|
||||||
|
- name: Apple Card
|
||||||
|
amount: -80.07
|
||||||
|
date: 2023-04-03
|
||||||
|
- name: Chase
|
||||||
|
amount: -0
|
||||||
|
date: 2023-03-16
|
||||||
|
- name: Citi
|
||||||
|
amount: -2307.84
|
||||||
|
date: 2023-04-06
|
||||||
|
- name: Discover
|
||||||
|
amount: -197.90
|
||||||
|
date: 2023-04-03
|
||||||
|
transactions:
|
||||||
|
- name: Bills
|
||||||
|
amount: -1690.00
|
||||||
|
date: first
|
||||||
|
frequency: monthly
|
||||||
|
delayable: false
|
||||||
|
- name: Car Insurance
|
||||||
|
amount: -140.92
|
||||||
|
date: first
|
||||||
|
frequency: monthly
|
||||||
|
delayable: false
|
||||||
|
- name: Savings
|
||||||
|
amount: -1000
|
||||||
|
date: 14
|
||||||
|
frequency: monthly
|
||||||
|
delayable: false
|
||||||
|
- name: Brokerage
|
||||||
|
amount: -1000
|
||||||
|
date: 2023-04-30
|
||||||
|
frequency: monthly
|
||||||
|
delayable: true
|
||||||
|
- name: Paycheck
|
||||||
|
amount: 3000
|
||||||
|
date: 15
|
||||||
|
frequency: biweekly
|
||||||
|
shift: early
|
||||||
10
go.mod
10
go.mod
@@ -1,13 +1,11 @@
|
|||||||
module github.com/samuelattwood/cashflow
|
module github.com/samuelattwood/cashflow
|
||||||
|
|
||||||
go 1.19
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/sirupsen/logrus v1.9.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/sirupsen/logrus v1.9.3
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require golang.org/x/sys v0.32.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
|
||||||
)
|
|
||||||
|
|||||||
11
go.sum
11
go.sum
@@ -1,17 +1,18 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
728
main.go
728
main.go
@@ -5,7 +5,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,18 +18,48 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxWidth = 100
|
||||||
|
DelayWeekend DelayReason = "Weekend"
|
||||||
|
DelayInsufficient DelayReason = "Insufficient Funds"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DelayReason string
|
||||||
|
|
||||||
type timeSlice []time.Time
|
type timeSlice []time.Time
|
||||||
|
|
||||||
func (s timeSlice) Len() int { return len(s) }
|
func (s timeSlice) Len() int { return len(s) }
|
||||||
func (s timeSlice) Less(i, j int) bool { return s[i].Before(s[j]) }
|
func (s timeSlice) Less(i, j int) bool { return s[i].Before(s[j]) }
|
||||||
func (s timeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
func (s timeSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
||||||
|
type transactionList []*Transaction
|
||||||
|
|
||||||
|
func (t transactionList) Len() int { return len(t) }
|
||||||
|
func (t transactionList) Less(i, j int) bool {
|
||||||
|
if t[i].Date.Before(t[j].Date) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if t[i].Amount < 0 && t[j].Amount < 0 {
|
||||||
|
return math.Abs(t[i].Amount) > math.Abs(t[j].Amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return t[i].Amount > t[j].Amount
|
||||||
|
}
|
||||||
|
func (t transactionList) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Balance float64 `yaml:"balance"`
|
Balance float64 `yaml:"balance"`
|
||||||
Minimum float64 `yaml:"minimum"`
|
Minimum float64 `yaml:"minimum"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AccountYaml struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Balance string `yaml:"balance"`
|
||||||
|
Minimum string `yaml:"minimum"`
|
||||||
|
}
|
||||||
|
|
||||||
type Charge struct {
|
type Charge struct {
|
||||||
Name string
|
Name string
|
||||||
Late bool
|
Late bool
|
||||||
@@ -36,14 +69,13 @@ type Charge struct {
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Accounts []Account
|
Accounts []Account
|
||||||
Credit []Transaction
|
Transactions transactionList
|
||||||
Transactions []Transaction
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigYaml struct {
|
type ConfigYaml struct {
|
||||||
Start string
|
Start string
|
||||||
End string
|
End string
|
||||||
Accounts []Account
|
Accounts []AccountYaml
|
||||||
Credit []TransactionYaml
|
Credit []TransactionYaml
|
||||||
Transactions []TransactionYaml
|
Transactions []TransactionYaml
|
||||||
}
|
}
|
||||||
@@ -53,19 +85,19 @@ type Transaction struct {
|
|||||||
Date time.Time
|
Date time.Time
|
||||||
Delayable bool
|
Delayable bool
|
||||||
Frequency string
|
Frequency string
|
||||||
HardDate bool
|
|
||||||
ID string
|
ID string
|
||||||
Late bool
|
Delayed bool
|
||||||
|
DelayedReason DelayReason
|
||||||
Name string
|
Name string
|
||||||
OrigDate time.Time
|
OriginDate time.Time
|
||||||
Shift string
|
Shift string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TransactionYaml struct {
|
type TransactionYaml struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Amount float64 `yaml:"amount"`
|
Amount string `yaml:"amount"`
|
||||||
Date string `yaml:"date"`
|
Date string `yaml:"date"`
|
||||||
Delayable *bool `yaml:"delayable"`
|
Delayable string `yaml:"delayable"`
|
||||||
Frequency string `yaml:"frequency,omitempty"`
|
Frequency string `yaml:"frequency,omitempty"`
|
||||||
Shift string `yaml:"shift"`
|
Shift string `yaml:"shift"`
|
||||||
}
|
}
|
||||||
@@ -85,9 +117,9 @@ type LogDay struct {
|
|||||||
Transactions map[string]*Transaction
|
Transactions map[string]*Transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TransactionLog) dates() timeSlice {
|
func (tl *TransactionLog) dates() timeSlice {
|
||||||
dateSlice := make(timeSlice, 0)
|
dateSlice := make(timeSlice, 0)
|
||||||
for date, _ := range t.LogDays {
|
for date := range tl.LogDays {
|
||||||
dateSlice = append(dateSlice, date)
|
dateSlice = append(dateSlice, date)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,290 +128,409 @@ func (t TransactionLog) dates() timeSlice {
|
|||||||
return dateSlice
|
return dateSlice
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TransactionLog) first() time.Time {
|
func (t *TransactionLog) first() time.Time {
|
||||||
return t.dates()[0]
|
return t.dates()[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t TransactionLog) last() time.Time {
|
func (t *TransactionLog) last() time.Time {
|
||||||
return t.dates()[t.dates().Len()-1]
|
return t.dates()[t.dates().Len()-1]
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (logDay LogDay) transactions() []*Transaction {
|
func (logDay *LogDay) transactions() transactionList {
|
||||||
transactions := make([]*Transaction, 0)
|
transactions := transactionList{}
|
||||||
|
|
||||||
for _, t := range logDay.Transactions {
|
for _, t := range logDay.Transactions {
|
||||||
transactions = append(transactions, t)
|
transactions = append(transactions, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortTransactions(transactions)
|
sort.Sort(transactions)
|
||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextLogDay(transactionLog *TransactionLog, currentDate time.Time) *LogDay {
|
func (tl *TransactionLog) nextLogDay(currentDate time.Time) *LogDay {
|
||||||
var next time.Time
|
var next time.Time
|
||||||
for i, date := range transactionLog.dates() {
|
for i, date := range tl.dates() {
|
||||||
|
j := i + 1
|
||||||
|
if j >= len(tl.dates()) {
|
||||||
|
next = tl.dates()[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
if date == currentDate {
|
if date == currentDate {
|
||||||
next = transactionLog.dates()[i+1]
|
next = tl.dates()[j]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return transactionLog.LogDays[next]
|
return tl.LogDays[next]
|
||||||
}
|
}
|
||||||
|
|
||||||
func previousLogDay(transactionLog *TransactionLog, currentDate time.Time) *LogDay {
|
func (tl *TransactionLog) previousLogDay(currentDate time.Time) *LogDay {
|
||||||
var previous time.Time
|
var previous time.Time
|
||||||
for _, date := range transactionLog.dates() {
|
for _, date := range tl.dates() {
|
||||||
if date == currentDate {
|
if date == currentDate {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
previous = date
|
previous = date
|
||||||
}
|
}
|
||||||
|
|
||||||
return transactionLog.LogDays[previous]
|
return tl.LogDays[previous]
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseConfig(configYaml *ConfigYaml) Config {
|
func isWeekend(date time.Time) bool {
|
||||||
config := Config{
|
if date.Weekday() == 0 || date.Weekday() == 6 {
|
||||||
Accounts: configYaml.Accounts,
|
return true
|
||||||
Credit: make([]Transaction, 0),
|
}
|
||||||
Transactions: make([]Transaction, 0),
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDelayable(delayableString string) (bool, error) {
|
||||||
|
if delayableString == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delayable, err := strconv.ParseBool(delayableString)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return delayable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTransaction(transactionYaml TransactionYaml) (*Transaction, error) {
|
||||||
|
date, err := parseDate(transactionYaml.Date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
date = stripTime(date)
|
||||||
|
|
||||||
|
if transactionYaml.Shift == "" {
|
||||||
|
transactionYaml.Shift = "late"
|
||||||
|
}
|
||||||
|
|
||||||
|
delayable, err := parseDelayable(transactionYaml.Delayable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amountStr := strings.ReplaceAll(transactionYaml.Amount, "$", "")
|
||||||
|
amountStr = strings.ReplaceAll(amountStr, ",", "")
|
||||||
|
|
||||||
|
amount, err := strconv.ParseFloat(amountStr, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction := &Transaction{
|
||||||
|
Amount: amount,
|
||||||
|
Date: stripTime(date),
|
||||||
|
Delayable: delayable,
|
||||||
|
Frequency: transactionYaml.Frequency,
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Delayed: false,
|
||||||
|
Name: transactionYaml.Name,
|
||||||
|
OriginDate: date,
|
||||||
|
Shift: transactionYaml.Shift,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transaction, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfig(configYaml ConfigYaml) *Config {
|
||||||
|
accounts := make([]Account, 0)
|
||||||
|
for _, account := range configYaml.Accounts {
|
||||||
|
balanceStr := strings.ReplaceAll(account.Balance, "$", "")
|
||||||
|
balanceStr = strings.ReplaceAll(balanceStr, ",", "")
|
||||||
|
balance, err := strconv.ParseFloat(balanceStr, 32)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
var minimum float64 = 0
|
||||||
|
if account.Minimum != "" {
|
||||||
|
minimumStr := strings.ReplaceAll(account.Minimum, "$", "")
|
||||||
|
minimumStr = strings.ReplaceAll(minimumStr, ",", "")
|
||||||
|
minimum, err = strconv.ParseFloat(minimumStr, 32)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts = append(accounts, Account{
|
||||||
|
Name: account.Name,
|
||||||
|
Balance: balance,
|
||||||
|
Minimum: minimum,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Accounts: accounts,
|
||||||
|
Transactions: make([]*Transaction, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, transaction := range configYaml.Credit {
|
for _, transaction := range configYaml.Credit {
|
||||||
formalDate, hardDate := parseDate(transaction.Date)
|
newTransaction, err := parseTransaction(transaction)
|
||||||
if transaction.Delayable == nil {
|
if err != nil {
|
||||||
transaction.Delayable = &[]bool{true}[0]
|
logrus.Error(err)
|
||||||
}
|
}
|
||||||
if transaction.Shift == "" {
|
config.Transactions = append(config.Transactions, newTransaction)
|
||||||
transaction.Shift = "late"
|
logrus.Debugf("Parsed Transaction: %s", transaction.Name)
|
||||||
}
|
|
||||||
newTransaction := Transaction{
|
|
||||||
Amount: transaction.Amount,
|
|
||||||
Date: formalDate,
|
|
||||||
Delayable: *transaction.Delayable,
|
|
||||||
Frequency: transaction.Frequency,
|
|
||||||
HardDate: hardDate,
|
|
||||||
ID: uuid.NewString(),
|
|
||||||
Late: false,
|
|
||||||
Name: transaction.Name,
|
|
||||||
OrigDate: formalDate,
|
|
||||||
Shift: transaction.Shift,
|
|
||||||
}
|
|
||||||
config.Credit = append(config.Credit, newTransaction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, transaction := range configYaml.Transactions {
|
for _, transaction := range configYaml.Transactions {
|
||||||
formalDate, hardDate := parseDate(transaction.Date)
|
newTransaction, err := parseTransaction(transaction)
|
||||||
if transaction.Delayable == nil {
|
if err != nil {
|
||||||
transaction.Delayable = &[]bool{true}[0]
|
logrus.Error(err)
|
||||||
}
|
|
||||||
if transaction.Shift == "" {
|
|
||||||
transaction.Shift = "late"
|
|
||||||
}
|
|
||||||
newTransaction := Transaction{
|
|
||||||
Amount: transaction.Amount,
|
|
||||||
Date: formalDate,
|
|
||||||
Delayable: *transaction.Delayable,
|
|
||||||
Frequency: transaction.Frequency,
|
|
||||||
HardDate: hardDate,
|
|
||||||
ID: uuid.NewString(),
|
|
||||||
Late: false,
|
|
||||||
Name: transaction.Name,
|
|
||||||
OrigDate: formalDate,
|
|
||||||
Shift: transaction.Shift,
|
|
||||||
}
|
}
|
||||||
config.Transactions = append(config.Transactions, newTransaction)
|
config.Transactions = append(config.Transactions, newTransaction)
|
||||||
|
logrus.Debugf("Parsed Transaction: %s", transaction.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDate(date string) (time.Time, bool) {
|
func parseDate(dateString string) (time.Time, error) {
|
||||||
hardDate := true
|
currentDay := currentDay()
|
||||||
base := time.Now().AddDate(0, 0, -time.Now().Day()+1)
|
|
||||||
current := time.Date(base.Year(),
|
regMatch, err := regexp.MatchString("^[0-9]{4}-[0-9]{1,2}-[0-9]{1,2}$", dateString)
|
||||||
base.Month(), base.Day(), 0, 0, 0, 0, time.UTC)
|
|
||||||
dateString := string(date)
|
|
||||||
formalDate, err := time.Parse("2006-1-2", dateString)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
hardDate = false
|
return time.Time{}, err
|
||||||
if date == "first" {
|
|
||||||
formalDate = current
|
|
||||||
if formalDate.Before(time.Now()) {
|
|
||||||
formalDate = formalDate.AddDate(0, 1, 0)
|
|
||||||
}
|
}
|
||||||
} else if date == "last" {
|
|
||||||
formalDate = lastOfMonth(current)
|
if regMatch {
|
||||||
|
parsedDate, err := time.Parse("2006-1-2", dateString)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return parsedDate, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if dateString == "first" {
|
||||||
|
if currentDay.Day() == 1 {
|
||||||
|
return currentDay, nil
|
||||||
} else {
|
} else {
|
||||||
dateString = fmt.Sprintf("%d-%d-%s",
|
return firstOfMonth(currentDay).AddDate(0, 1, 0), nil
|
||||||
time.Now().Year(), int(time.Now().Month()), date)
|
|
||||||
formalDate, _ = time.Parse("2006-1-2", dateString)
|
|
||||||
if formalDate.Before(time.Now()) {
|
|
||||||
formalDate = formalDate.AddDate(0, 1, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return formalDate, hardDate
|
|
||||||
}
|
|
||||||
|
|
||||||
func preFetchDates(transactions []Transaction, transactionLog *TransactionLog) {
|
if dateString == "last" {
|
||||||
for _, transaction := range transactions {
|
return lastOfMonth(currentDay), nil
|
||||||
if _, ok := transactionLog.LogDays[transaction.Date]; !ok && transaction.Amount != 0 {
|
|
||||||
logDay := LogDay{
|
|
||||||
Date: transaction.Date,
|
|
||||||
Transactions: make(map[string]*Transaction),
|
|
||||||
}
|
|
||||||
transactionLog.LogDays[transaction.Date] = &logDay
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logDates := transactionLog.dates()
|
|
||||||
transactionLog.StartDate = logDates[0]
|
|
||||||
transactionLog.EndDate = logDates[len(logDates)-1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectTransactions(transactions []Transaction, transactionLog *TransactionLog) {
|
dateString = fmt.Sprintf("%d-%d-%s", currentDay.Year(), currentDay.Month(), dateString)
|
||||||
|
parsedDate, err := time.Parse("2006-1-2", dateString)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedDate.Before(currentDay) {
|
||||||
|
return parsedDate.AddDate(0, 1, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedDate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingestTransactions(transactions []*Transaction, transactionLog *TransactionLog) {
|
||||||
for _, transaction := range transactions {
|
for _, transaction := range transactions {
|
||||||
if math.Abs(transaction.Amount) != 0 {
|
if math.Abs(transaction.Amount) != 0 {
|
||||||
addTransaction(transactionLog, &transaction)
|
transactionLog.addTransaction(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortTransactions(transactions []*Transaction) {
|
func (tl *TransactionLog) addTransaction(transaction *Transaction) {
|
||||||
sort.Slice(transactions, func(i, j int) bool {
|
if transaction.Frequency == "" {
|
||||||
if transactions[i].Amount < 0 && transactions[j].Amount < 0 {
|
tl.addDay(transaction.Date)
|
||||||
return math.Abs(transactions[i].Amount) > math.Abs(transactions[j].Amount)
|
tl.LogDays[transaction.Date].Transactions[transaction.ID] = transaction
|
||||||
} else {
|
tl.Transactions[transaction.ID] = transaction
|
||||||
return transactions[i].Amount > transactions[j].Amount
|
|
||||||
}
|
logrus.Debugf("Adding transaction %s (%s | %f) to %s",
|
||||||
})
|
transaction.ID, transaction.Name, transaction.Amount, transaction.Date.String())
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTransaction(transactionLog *TransactionLog, transaction *Transaction) {
|
dateList := calculateFrequency(transaction.Frequency,
|
||||||
var dateList []time.Time
|
transaction.Date, tl.EndDate, transaction.Delayable)
|
||||||
if transaction.Frequency != "" {
|
|
||||||
dateList = calculateFrequency(transaction.Frequency,
|
|
||||||
transaction.Date, transactionLog.EndDate, transaction.HardDate)
|
|
||||||
} else {
|
|
||||||
dateList = []time.Time{transaction.Date}
|
|
||||||
}
|
|
||||||
for _, date := range dateList {
|
for _, date := range dateList {
|
||||||
if _, ok := transactionLog.LogDays[date]; !ok {
|
tl.addDay(date)
|
||||||
logDay := LogDay{
|
t := &Transaction{
|
||||||
Date: date,
|
|
||||||
Transactions: make(map[string]*Transaction),
|
|
||||||
}
|
|
||||||
transactionLog.LogDays[date] = &logDay
|
|
||||||
}
|
|
||||||
t := Transaction{
|
|
||||||
Amount: transaction.Amount,
|
Amount: transaction.Amount,
|
||||||
Date: date,
|
Date: date,
|
||||||
Delayable: transaction.Delayable,
|
Delayable: transaction.Delayable,
|
||||||
Frequency: "",
|
Frequency: "",
|
||||||
HardDate: false,
|
|
||||||
ID: uuid.NewString(),
|
ID: uuid.NewString(),
|
||||||
Late: false,
|
Delayed: false,
|
||||||
Name: transaction.Name,
|
Name: transaction.Name,
|
||||||
OrigDate: date,
|
OriginDate: date,
|
||||||
Shift: transaction.Shift,
|
Shift: transaction.Shift,
|
||||||
}
|
}
|
||||||
transactionLog.LogDays[date].Transactions[t.ID] = &t
|
tl.LogDays[date].Transactions[t.ID] = t
|
||||||
transactionLog.Transactions[t.ID] = &t
|
tl.Transactions[t.ID] = t
|
||||||
|
|
||||||
|
logrus.Debugf("Adding transaction %s (%s | %f) to %s",
|
||||||
|
t.ID, t.Name, t.Amount, t.Date.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *TransactionLog) addDay(date time.Time) {
|
||||||
|
if _, ok := tl.LogDays[date]; !ok {
|
||||||
|
logrus.Debugf("Adding Log Day %s", stripTime(date).String())
|
||||||
|
tl.LogDays[date] = &LogDay{
|
||||||
|
Date: stripTime(date),
|
||||||
|
Transactions: make(map[string]*Transaction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tl *TransactionLog) deleteDay(date time.Time) {
|
||||||
|
if _, ok := tl.LogDays[date]; ok {
|
||||||
|
logrus.Debugf("Deleting Log Day %s", date.String())
|
||||||
|
delete(tl.LogDays, date)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateShift(transactionLog TransactionLog, transaction Transaction) time.Time {
|
func calculateShift(transactionLog TransactionLog, transaction Transaction) time.Time {
|
||||||
|
if isWeekend(transaction.Date) {
|
||||||
|
add := 1
|
||||||
newDate := transaction.Date
|
newDate := transaction.Date
|
||||||
if transaction.Late {
|
|
||||||
if transaction.Shift == "early" {
|
if transaction.Shift == "early" {
|
||||||
newDate = newDate.AddDate(0, 0, -1)
|
add = -1
|
||||||
} else {
|
|
||||||
newDate = newDate.AddDate(0, 0, 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for newDate.Weekday() == 0 || newDate.Weekday() == 6 {
|
for isWeekend(newDate) {
|
||||||
if transaction.Shift == "early" {
|
newDate = newDate.AddDate(0, 0, add)
|
||||||
newDate = newDate.AddDate(0, 0, -1)
|
}
|
||||||
} else {
|
|
||||||
newDate = newDate.AddDate(0, 0, 1)
|
if newDate.Before(transactionLog.first()) || newDate.After(transactionLog.last()) {
|
||||||
|
for isWeekend(newDate) {
|
||||||
|
newDate = newDate.AddDate(0, 0, add)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newDate
|
return newDate
|
||||||
}
|
}
|
||||||
|
|
||||||
func delayTransaction(transactionLog *TransactionLog, transaction *Transaction) {
|
if transaction.Shift == "early" {
|
||||||
if transaction.Date.Weekday() != 0 && transaction.Date.Weekday() != 6 {
|
return transactionLog.previousLogDay(transaction.Date).Date
|
||||||
transaction.Late = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newDate := calculateShift(*transactionLog, *transaction)
|
return transactionLog.nextLogDay(transaction.Date).Date
|
||||||
if newDate.Before(transactionLog.first()) || newDate.After(transactionLog.last()) || newDate == transaction.Date {
|
}
|
||||||
|
|
||||||
|
func (tl *TransactionLog) delayTransaction(transaction *Transaction) bool {
|
||||||
|
newDate := calculateShift(*tl, *transaction)
|
||||||
|
|
||||||
|
if newDate.Before(currentDay()) {
|
||||||
|
logrus.Debugf("Transaction presumed executed in the past %s (%s | %f)",
|
||||||
|
transaction.ID, transaction.Name, transaction.Amount)
|
||||||
|
delete(tl.LogDays[transaction.Date].Transactions, transaction.ID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if newDate.Before(tl.first()) || newDate == transaction.Date {
|
||||||
|
logrus.Debugf("Transaction not delayable %s (%s | %f)", transaction.ID, transaction.Name, transaction.Amount)
|
||||||
transaction.Delayable = false
|
transaction.Delayable = false
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := transactionLog.LogDays[newDate]; !ok {
|
delete(tl.LogDays[transaction.Date].Transactions, transaction.ID)
|
||||||
logDay := LogDay{
|
|
||||||
Date: newDate,
|
if newDate.After(tl.last()) {
|
||||||
Transactions: map[string]*Transaction{},
|
newDate = tl.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionLog.LogDays[newDate] = &logDay
|
for isWeekend(newDate) {
|
||||||
|
newDate = newDate.AddDate(0, 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(transactionLog.LogDays[transaction.Date].Transactions, transaction.ID)
|
tl.addDay(newDate)
|
||||||
|
|
||||||
transaction.Date = newDate
|
transaction.Date = newDate
|
||||||
transactionLog.LogDays[newDate].Transactions[transaction.ID] = transaction
|
tl.LogDays[newDate].Transactions[transaction.ID] = transaction
|
||||||
|
|
||||||
|
transaction.Delayed = true
|
||||||
|
if transaction.DelayedReason == "" {
|
||||||
|
transaction.DelayedReason = DelayInsufficient
|
||||||
}
|
}
|
||||||
|
|
||||||
func executeTransaction(transactionLog *TransactionLog, transaction *Transaction) bool {
|
return true
|
||||||
logDay := transactionLog.LogDays[transaction.Date]
|
}
|
||||||
|
|
||||||
|
func (tl *TransactionLog) executeTransaction(transaction *Transaction) bool {
|
||||||
|
logDay := tl.LogDays[transaction.Date]
|
||||||
accounts := make([]Account, len(logDay.EndingBalance))
|
accounts := make([]Account, len(logDay.EndingBalance))
|
||||||
copy(accounts, logDay.EndingBalance)
|
copy(accounts, logDay.EndingBalance)
|
||||||
executed := false
|
|
||||||
finalDate := transactionLog.EndDate
|
if isWeekend(transaction.Date) && transaction.Delayable {
|
||||||
if transaction.Date.Weekday() == 0 || transaction.Date.Weekday() == 6 && transaction.Delayable {
|
delayed := tl.delayTransaction(transaction)
|
||||||
logrus.Debugf("Delaying %s due to weekend scheduling\n", transaction.Name)
|
if delayed {
|
||||||
delayTransaction(transactionLog, transaction)
|
logrus.Debugf("Delaying %s to %s due to weekend scheduling\n", transaction.Name, transaction.Date.String())
|
||||||
} else if transaction.Amount > 0 {
|
transaction.DelayedReason = DelayWeekend
|
||||||
logrus.Debugf("Executing Credit: %s %s (%f)\n", logDay.Date, transaction.Name, transaction.Amount)
|
|
||||||
accounts[0].Balance = accounts[0].Balance + transaction.Amount
|
|
||||||
executed = true
|
|
||||||
} else if transaction.Amount < 0 && math.Abs(transaction.Amount) > (accounts[0].Balance-accounts[0].Minimum) && transaction.Date.Before(finalDate) && transaction.Delayable {
|
|
||||||
logrus.Debugf("Balance: %f | Minimum: %f\n", accounts[0].Balance, accounts[0].Minimum)
|
|
||||||
logrus.Debugf("Delaying %s: %f (%s) due to lack of funds\n", transaction.Name, transaction.Amount, transaction.Date)
|
|
||||||
delayTransaction(transactionLog, transaction)
|
|
||||||
} else if transaction.Amount < 0 {
|
|
||||||
logrus.Debugf("Executing Charge: %s %s (%f)\n", logDay.Date, transaction.Name, transaction.Amount)
|
|
||||||
accounts[0].Balance = accounts[0].Balance + transaction.Amount
|
|
||||||
executed = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Balance: %f | Minimum: %f\n", accounts[0].Balance, accounts[0].Minimum)
|
||||||
|
logrus.Debugf("Delayable: %t", transaction.Delayable)
|
||||||
|
if !isAffordable(transaction.Amount, accounts[0].Balance, accounts[0].Minimum) && transaction.Delayable {
|
||||||
|
tl.delayTransaction(transaction)
|
||||||
|
logrus.Debugf("Delaying %s to %s: %f (%s) due to lack of funds\n",
|
||||||
|
transaction.Name, transaction.Date.String(), transaction.Amount, transaction.Date)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Executing: %s (%s | %s | %f)",
|
||||||
|
transaction.ID, logDay.Date.String(), transaction.Name, transaction.Amount)
|
||||||
|
|
||||||
|
accounts[0].Balance = accounts[0].Balance + transaction.Amount
|
||||||
|
|
||||||
logDay.EndingBalance = make([]Account, len(accounts))
|
logDay.EndingBalance = make([]Account, len(accounts))
|
||||||
copy(logDay.EndingBalance, accounts)
|
copy(logDay.EndingBalance, accounts)
|
||||||
|
|
||||||
return executed
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAffordable(charge, balance, minimum float64) bool {
|
||||||
|
if charge > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Debugf("Charge: %f | Balance - Miniumum: %f", charge, balance-minimum)
|
||||||
|
logrus.Debugf("abs(charge) < (balance - minimum): %t", math.Abs(charge) < (balance-minimum))
|
||||||
|
|
||||||
|
return math.Abs(charge) < (balance - minimum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentDay() time.Time {
|
||||||
|
return stripTime(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentTZ() *time.Location {
|
||||||
|
return time.Now().Local().Location()
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstOfMonth(t time.Time) time.Time {
|
func firstOfMonth(t time.Time) time.Time {
|
||||||
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC)
|
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, currentTZ())
|
||||||
}
|
}
|
||||||
|
|
||||||
func lastOfMonth(t time.Time) time.Time {
|
func lastOfMonth(t time.Time) time.Time {
|
||||||
return time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, time.UTC)
|
return t.AddDate(0, 1, -t.Day())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripTime(t time.Time) time.Time {
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, currentTZ())
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateFrequency(frequency string, startDate time.Time, endDate time.Time, hardDate bool) []time.Time {
|
func calculateFrequency(frequency string, startDate time.Time, endDate time.Time, hardDate bool) []time.Time {
|
||||||
base := time.Now()
|
current := currentDay()
|
||||||
current := time.Date(base.Year(),
|
|
||||||
base.Month(), base.Day(), 0, 0, 0, 0, time.UTC)
|
|
||||||
initDate := startDate
|
initDate := startDate
|
||||||
dateList := make([]time.Time, 0)
|
dateList := make([]time.Time, 0)
|
||||||
|
|
||||||
for initDate.Before(endDate.AddDate(0, 0, 1)) {
|
for initDate.Before(endDate.AddDate(0, 0, 1)) {
|
||||||
if !initDate.Before(current) || hardDate {
|
if !initDate.Before(current) || hardDate {
|
||||||
dateList = append(dateList, initDate)
|
dateList = append(dateList, initDate)
|
||||||
@@ -397,9 +548,10 @@ func calculateFrequency(frequency string, startDate time.Time, endDate time.Time
|
|||||||
initDate = initDate.AddDate(0, 1, 0)
|
initDate = initDate.AddDate(0, 1, 0)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break
|
logrus.Fatal("Invalid Frequency")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dateList
|
return dateList
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +565,7 @@ func categorizeTransactions(transactions map[string]*Transaction) map[string][]s
|
|||||||
for _, t := range transactions {
|
for _, t := range transactions {
|
||||||
if t.Amount > 0 {
|
if t.Amount > 0 {
|
||||||
categorized["credits"] = append(categorized["credits"], t.ID)
|
categorized["credits"] = append(categorized["credits"], t.ID)
|
||||||
} else if t.Late {
|
} else if t.Delayed {
|
||||||
categorized["late"] = append(categorized["late"], t.ID)
|
categorized["late"] = append(categorized["late"], t.ID)
|
||||||
} else {
|
} else {
|
||||||
categorized["standard"] = append(categorized["standard"], t.ID)
|
categorized["standard"] = append(categorized["standard"], t.ID)
|
||||||
@@ -423,70 +575,54 @@ func categorizeTransactions(transactions map[string]*Transaction) map[string][]s
|
|||||||
return categorized
|
return categorized
|
||||||
}
|
}
|
||||||
|
|
||||||
func processTransactionLog(transactionLog *TransactionLog, accounts []Account) bool {
|
func (tl *TransactionLog) processTransactions(accounts []Account) bool {
|
||||||
shifted := false
|
shifted := false
|
||||||
endBalance := accounts
|
endBalance := accounts
|
||||||
logrus.Debugf("\n\nITERATION %d\n\n", transactionLog.Iteration)
|
|
||||||
if transactionLog.Iteration == 0 {
|
logrus.Debugf("\n\nITERATION %d\n\n", tl.Iteration)
|
||||||
for _, date := range transactionLog.dates() {
|
for _, date := range tl.dates() {
|
||||||
logDay := transactionLog.LogDays[date]
|
logrus.Debugf("Processing Log Day %s", date.String())
|
||||||
|
logDay := tl.LogDays[date]
|
||||||
logDay.StartingBalance = make([]Account, len(endBalance))
|
logDay.StartingBalance = make([]Account, len(endBalance))
|
||||||
logDay.EndingBalance = make([]Account, len(endBalance))
|
logDay.EndingBalance = make([]Account, len(endBalance))
|
||||||
copy(logDay.StartingBalance, endBalance)
|
copy(logDay.StartingBalance, endBalance)
|
||||||
copy(logDay.EndingBalance, endBalance)
|
copy(logDay.EndingBalance, endBalance)
|
||||||
categorized := categorizeTransactions(logDay.Transactions)
|
categorized := categorizeTransactions(logDay.Transactions)
|
||||||
logrus.Debugf("---\nStarting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance)
|
if processTransactionsByID(tl, categorized["credits"]) {
|
||||||
logrus.Debugln(categorized)
|
|
||||||
if processTransactions(transactionLog, categorized["credits"]) {
|
|
||||||
shifted = true
|
shifted = true
|
||||||
}
|
}
|
||||||
logrus.Debugf("Starting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance)
|
if processTransactionsByID(tl, categorized["late"]) {
|
||||||
logrus.Debugf("Ending Balance %s: %f\n", date, logDay.EndingBalance[0].Balance)
|
|
||||||
endBalance = logDay.EndingBalance
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for _, date := range transactionLog.dates() {
|
|
||||||
logDay := transactionLog.LogDays[date]
|
|
||||||
logDay.StartingBalance = make([]Account, len(endBalance))
|
|
||||||
logDay.EndingBalance = make([]Account, len(endBalance))
|
|
||||||
copy(logDay.StartingBalance, endBalance)
|
|
||||||
copy(logDay.EndingBalance, endBalance)
|
|
||||||
categorized := categorizeTransactions(logDay.Transactions)
|
|
||||||
logrus.Debugf("---\nStarting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance)
|
|
||||||
logrus.Debugln(categorized)
|
|
||||||
if processTransactions(transactionLog, categorized["credits"]) {
|
|
||||||
shifted = true
|
shifted = true
|
||||||
}
|
}
|
||||||
if processTransactions(transactionLog, categorized["late"]) {
|
if processTransactionsByID(tl, categorized["standard"]) {
|
||||||
shifted = true
|
shifted = true
|
||||||
}
|
}
|
||||||
if processTransactions(transactionLog, categorized["standard"]) {
|
logrus.Debugf("Starting Balance %s: %f", date, logDay.StartingBalance[0].Balance)
|
||||||
shifted = true
|
logrus.Debugf("Ending Balance %s: %f", date, logDay.EndingBalance[0].Balance)
|
||||||
}
|
|
||||||
logrus.Debugf("Starting Balance %s: %f\n", date, logDay.StartingBalance[0].Balance)
|
|
||||||
logrus.Debugf("Ending Balance %s: %f\n", date, logDay.EndingBalance[0].Balance)
|
|
||||||
endBalance = logDay.EndingBalance
|
endBalance = logDay.EndingBalance
|
||||||
if shifted {
|
if shifted {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if len(logDay.Transactions) == 0 {
|
if len(logDay.Transactions) == 0 {
|
||||||
delete(transactionLog.LogDays, logDay.Date)
|
tl.deleteDay(date)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionLog.Iteration++
|
tl.Iteration++
|
||||||
return shifted
|
return shifted
|
||||||
}
|
}
|
||||||
|
|
||||||
func processTransactions(transactionLog *TransactionLog, transactionIDs []string) bool {
|
func processTransactionsByID(transactionLog *TransactionLog, transactionIDs []string) bool {
|
||||||
shifted := false
|
shifted := false
|
||||||
|
|
||||||
for _, id := range transactionIDs {
|
for _, id := range transactionIDs {
|
||||||
transaction, ok := transactionLog.Transactions[id]
|
transaction, ok := transactionLog.Transactions[id]
|
||||||
if !ok {
|
if !ok {
|
||||||
logrus.Fatalf("Transaction ID: %s not found in log\n", id)
|
logrus.Fatalf("Transaction ID: %s not found in log\n", id)
|
||||||
}
|
}
|
||||||
if !executeTransaction(transactionLog, transaction) {
|
logrus.Debugf("Processing %s (%s | %f)", id, transaction.Name, transaction.Amount)
|
||||||
|
if !transactionLog.executeTransaction(transaction) {
|
||||||
|
logrus.Debugf("Shifted due to: %s", transaction.Name)
|
||||||
shifted = true
|
shifted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -494,76 +630,129 @@ func processTransactions(transactionLog *TransactionLog, transactionIDs []string
|
|||||||
return shifted
|
return shifted
|
||||||
}
|
}
|
||||||
|
|
||||||
func printOutput(logDay LogDay) {
|
func getTerminalWidth() int {
|
||||||
delim := strings.Repeat("=", 67)
|
cmd := exec.Command("stty", "size")
|
||||||
divider := delim
|
cmd.Stdin = os.Stdin
|
||||||
delim2 := strings.Repeat("-", 66)
|
out, err := cmd.Output()
|
||||||
divider2 := delim2 + "|"
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
widthStr := string(out[:len(out)-1])
|
||||||
|
sttyOut := strings.Split(widthStr, " ")
|
||||||
|
width, err := strconv.Atoi(sttyOut[1])
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func printWithMaxWidth(maxWidth int, suffix string, strs ...string) {
|
||||||
|
total := 0
|
||||||
|
whiteSpace := regexp.MustCompile(`^\s+$`)
|
||||||
|
for _, str := range strs {
|
||||||
|
line := ""
|
||||||
|
if whiteSpace.MatchString(str) {
|
||||||
|
fmt.Print(str)
|
||||||
|
total += len(str)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
words := strings.Fields(str)
|
||||||
|
for _, word := range words {
|
||||||
|
total += len(word)
|
||||||
|
if len(line)+len(word)+1 >= maxWidth {
|
||||||
|
fmt.Println(line)
|
||||||
|
line = ""
|
||||||
|
}
|
||||||
|
if line == "" {
|
||||||
|
line = word
|
||||||
|
} else {
|
||||||
|
line = line + " " + word
|
||||||
|
total += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatCount := maxWidth - total - len(suffix)
|
||||||
|
if repeatCount < 0 {
|
||||||
|
repeatCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate := strings.Repeat(" ", repeatCount) + suffix
|
||||||
|
|
||||||
|
fmt.Println(terminate)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTransactionLine(amount float64, name, prefix, suffix string, maxWidth int) {
|
||||||
|
segment := fmt.Sprintf("%s$%.2f", prefix, math.Abs(amount))
|
||||||
|
spacer := strings.Repeat(" ", (maxWidth/4)-len(segment))
|
||||||
|
printWithMaxWidth(maxWidth, suffix, segment, spacer, fmt.Sprintf("| %s", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func printOutput(logDay LogDay) {
|
||||||
|
|
||||||
|
fullWidth := getTerminalWidth()
|
||||||
|
|
||||||
|
if fullWidth > maxWidth {
|
||||||
|
fullWidth = maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
border := strings.Repeat("=", fullWidth)
|
||||||
|
divider := strings.Repeat("-", fullWidth-1) + "|"
|
||||||
|
|
||||||
|
fmt.Println(border)
|
||||||
|
printWithMaxWidth(fullWidth, "|", logDay.Date.Format("Jan 02 2006"))
|
||||||
|
|
||||||
fmt.Println(divider)
|
|
||||||
outString := fmt.Sprint(logDay.Date.Format("Jan 02 2006"))
|
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(outString))
|
|
||||||
fmt.Printf("%s%s |\n", outString, rear_spacer)
|
|
||||||
for _, v := range logDay.StartingBalance {
|
for _, v := range logDay.StartingBalance {
|
||||||
spacer := strings.Repeat(" ", 19-len(fmt.Sprintf("%.2f", v.Balance)))
|
printTransactionLine(v.Balance, v.Name, "", "|", fullWidth)
|
||||||
outString := fmt.Sprintf("$%.2f%s| %s", v.Balance, spacer, v.Name)
|
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(outString))
|
|
||||||
fmt.Printf("%s%s |\n", outString, rear_spacer)
|
|
||||||
}
|
}
|
||||||
for _, charge := range logDay.transactions() {
|
for _, charge := range logDay.transactions() {
|
||||||
spacer := strings.Repeat(" ", 18-len(fmt.Sprintf("%.2f", math.Abs(charge.Amount))))
|
prefix := "-"
|
||||||
outString := ""
|
suffix := "|"
|
||||||
if charge.Amount <= 0 {
|
if charge.Amount > 0 {
|
||||||
outString = "-"
|
prefix = "+"
|
||||||
} else {
|
|
||||||
outString = "+"
|
|
||||||
}
|
}
|
||||||
outString = fmt.Sprintf("%s$%.2f%s| %s", outString, math.Abs(charge.Amount), spacer, charge.Name)
|
|
||||||
if charge.Late {
|
if charge.DelayedReason != "" {
|
||||||
late_tag := fmt.Sprintf("(%s - %s)", "Late", charge.OrigDate.Format("Jan 02 2006"))
|
suffix = fmt.Sprintf("(%s: %s - %s) |", "Delayed", charge.DelayedReason, charge.OriginDate.Format("Jan 02 2006"))
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(late_tag)-len(outString))
|
|
||||||
fmt.Printf("%s%s%s |\n", outString, rear_spacer, late_tag)
|
|
||||||
} else if charge.Date != charge.OrigDate {
|
|
||||||
weekend_tag := fmt.Sprintf("(%s - %s)", "Weekend", charge.OrigDate.Format("Jan 02 2006"))
|
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(weekend_tag)-len(outString))
|
|
||||||
fmt.Printf("%s%s%s |\n", outString, rear_spacer, weekend_tag)
|
|
||||||
} else {
|
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(outString))
|
|
||||||
fmt.Printf("%s%s |\n", outString, rear_spacer)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
printTransactionLine(charge.Amount, charge.Name, prefix, suffix, fullWidth)
|
||||||
}
|
}
|
||||||
fmt.Println(divider2)
|
|
||||||
|
fmt.Println(divider)
|
||||||
for _, v := range logDay.EndingBalance {
|
for _, v := range logDay.EndingBalance {
|
||||||
spacer := strings.Repeat(" ", 19-len(fmt.Sprintf("%.2f", v.Balance)))
|
prefix := ""
|
||||||
outString := fmt.Sprintf("$%.2f%s| %s", v.Balance, spacer, v.Name)
|
if v.Balance < 0 {
|
||||||
rear_spacer := strings.Repeat(" ", 65-len(outString))
|
prefix = "-"
|
||||||
fmt.Printf("%s%s |\n", outString, rear_spacer)
|
|
||||||
}
|
}
|
||||||
fmt.Println(divider + "\n")
|
printTransactionLine(v.Balance, v.Name, prefix, "|", fullWidth)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\n\n", border)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
yfile, err := os.ReadFile(os.Args[1])
|
if len(os.Getenv("DEBUG")) > 0 {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
yfile, err := os.ReadFile(os.Args[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configYaml ConfigYaml
|
var configYaml ConfigYaml
|
||||||
|
|
||||||
transactionLog := TransactionLog{
|
|
||||||
LogDays: make(map[time.Time]*LogDay),
|
|
||||||
Transactions: make(map[string]*Transaction),
|
|
||||||
}
|
|
||||||
|
|
||||||
err = yaml.Unmarshal(yfile, &configYaml)
|
err = yaml.Unmarshal(yfile, &configYaml)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config := parseConfig(&configYaml)
|
transactionLog := &TransactionLog{
|
||||||
preFetchDates(config.Credit, &transactionLog)
|
LogDays: make(map[time.Time]*LogDay),
|
||||||
preFetchDates(config.Transactions, &transactionLog)
|
Transactions: make(map[string]*Transaction),
|
||||||
|
}
|
||||||
|
|
||||||
if configYaml.Start != "" {
|
if configYaml.Start != "" {
|
||||||
transactionLog.StartDate, err = time.Parse("2006-1-2", configYaml.Start)
|
transactionLog.StartDate, err = time.Parse("2006-1-2", configYaml.Start)
|
||||||
@@ -579,15 +768,24 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
collectTransactions(config.Credit, &transactionLog)
|
config := parseConfig(configYaml)
|
||||||
collectTransactions(config.Transactions, &transactionLog)
|
|
||||||
|
sort.Sort(config.Transactions)
|
||||||
|
|
||||||
|
if transactionLog.StartDate == (time.Time{}) {
|
||||||
|
transactionLog.StartDate = config.Transactions[0].Date
|
||||||
|
}
|
||||||
|
|
||||||
|
if transactionLog.EndDate == (time.Time{}) {
|
||||||
|
transactionLog.EndDate = config.Transactions[len(config.Transactions)-1].Date
|
||||||
|
}
|
||||||
|
|
||||||
|
ingestTransactions(config.Transactions, transactionLog)
|
||||||
|
|
||||||
shifted := true
|
shifted := true
|
||||||
for shifted {
|
for shifted {
|
||||||
//startingBalance := make([]Account, len(config.Accounts))
|
|
||||||
//copy(startingBalance, config.Accounts)
|
|
||||||
shifted = false
|
shifted = false
|
||||||
shifted = processTransactionLog(&transactionLog, config.Accounts)
|
shifted = transactionLog.processTransactions(config.Accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, date := range transactionLog.dates() {
|
for _, date := range transactionLog.dates() {
|
||||||
|
|||||||
Reference in New Issue
Block a user