Add delayed reason

This commit is contained in:
2025-04-28 22:13:00 -04:00
parent fc228d329e
commit 85721d7815
4 changed files with 118 additions and 27 deletions

50
example.yaml Normal file
View 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

6
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/samuelattwood/cashflow module github.com/samuelattwood/cashflow
go 1.20 go 1.24
require ( require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require golang.org/x/sys v0.9.0 // indirect require golang.org/x/sys v0.32.0 // indirect

11
go.sum
View File

@@ -1,21 +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.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/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.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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=

78
main.go
View File

@@ -19,9 +19,13 @@ import (
) )
const ( const (
maxWidth = 100 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) }
@@ -50,6 +54,12 @@ type Account struct {
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
@@ -65,21 +75,22 @@ type Config struct {
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
} }
type Transaction struct { type Transaction struct {
Amount float64 Amount float64
Date time.Time Date time.Time
Delayable bool Delayable bool
Frequency string Frequency string
ID string ID string
Delayed bool Delayed bool
Name string DelayedReason DelayReason
OriginDate time.Time Name string
Shift string OriginDate time.Time
Shift string
} }
type TransactionYaml struct { type TransactionYaml struct {
@@ -228,8 +239,33 @@ func parseTransaction(transactionYaml TransactionYaml) (*Transaction, error) {
} }
func parseConfig(configYaml ConfigYaml) *Config { 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{ config := &Config{
Accounts: configYaml.Accounts, Accounts: accounts,
Transactions: make([]*Transaction, 0), Transactions: make([]*Transaction, 0),
} }
@@ -368,6 +404,12 @@ func calculateShift(transactionLog TransactionLog, transaction Transaction) time
newDate = newDate.AddDate(0, 0, add) newDate = newDate.AddDate(0, 0, add)
} }
if newDate.Before(transactionLog.first()) || newDate.After(transactionLog.last()) {
for isWeekend(newDate) {
newDate = newDate.AddDate(0, 0, add)
}
}
return newDate return newDate
} }
@@ -391,7 +433,7 @@ func (tl *TransactionLog) delayTransaction(transaction *Transaction) {
newDate = tl.last() newDate = tl.last()
} }
if isWeekend(newDate) { for isWeekend(newDate) {
newDate = newDate.AddDate(0, 0, 1) newDate = newDate.AddDate(0, 0, 1)
} }
@@ -403,6 +445,9 @@ func (tl *TransactionLog) delayTransaction(transaction *Transaction) {
tl.LogDays[newDate].Transactions[transaction.ID] = transaction tl.LogDays[newDate].Transactions[transaction.ID] = transaction
transaction.Delayed = true transaction.Delayed = true
if transaction.DelayedReason == "" {
transaction.DelayedReason = DelayInsufficient
}
} }
@@ -414,6 +459,7 @@ func (tl *TransactionLog) executeTransaction(transaction *Transaction) bool {
if isWeekend(transaction.Date) && transaction.Delayable { if isWeekend(transaction.Date) && transaction.Delayable {
logrus.Debugf("Delaying %s due to weekend scheduling\n", transaction.Name) logrus.Debugf("Delaying %s due to weekend scheduling\n", transaction.Name)
tl.delayTransaction(transaction) tl.delayTransaction(transaction)
transaction.DelayedReason = DelayWeekend
return false return false
} }
@@ -658,10 +704,8 @@ func printOutput(logDay LogDay) {
prefix = "+" prefix = "+"
} }
if charge.Delayed { if charge.DelayedReason != "" {
suffix = fmt.Sprintf("(%s - %s) |", "Late", charge.OriginDate.Format("Jan 02 2006")) suffix = fmt.Sprintf("(%s: %s - %s) |", "Delayed", charge.DelayedReason, charge.OriginDate.Format("Jan 02 2006"))
} else if charge.Date != charge.OriginDate {
suffix = fmt.Sprintf("(%s - %s) |", "Weekend", charge.OriginDate.Format("Jan 02 2006"))
} }
printTransactionLine(charge.Amount, charge.Name, prefix, suffix, fullWidth) printTransactionLine(charge.Amount, charge.Name, prefix, suffix, fullWidth)