From a7b4a1ab0cdd370653905c5db9dffadb5ba1d62b Mon Sep 17 00:00:00 2001 From: Anantha Kumaran Date: Tue, 19 Apr 2022 13:32:05 +0530 Subject: [PATCH] add the concept of interest transaction If the posting amount is being debited from Income:Interest:*** account, then consider it as an interest transaction. --- internal/server/investment.go | 6 ++-- internal/server/ledger.go | 21 ++++++++++--- internal/server/overview.go | 12 ++++++-- internal/server/server.go | 5 +++- internal/service/interest.go | 55 +++++++++++++++++++++++++++++++++++ internal/service/market.go | 31 +++++++++++--------- 6 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 internal/service/interest.go diff --git a/internal/server/investment.go b/internal/server/investment.go index 4c889ee..81c7bad 100644 --- a/internal/server/investment.go +++ b/internal/server/investment.go @@ -1,16 +1,18 @@ package server import ( - "log" - "github.com/ananthakumaran/paisa/internal/model/posting" + "github.com/ananthakumaran/paisa/internal/service" "github.com/gin-gonic/gin" + "github.com/samber/lo" + log "github.com/sirupsen/logrus" "gorm.io/gorm" ) func GetInvestment(db *gorm.DB) gin.H { var postings []posting.Posting result := db.Where("account like ?", "Asset:%").Find(&postings) + postings = lo.Filter(postings, func(p posting.Posting, _ int) bool { return !service.IsInterest(db, p) }) if result.Error != nil { log.Fatal(result.Error) } diff --git a/internal/server/ledger.go b/internal/server/ledger.go index 4f25ef2..cddc669 100644 --- a/internal/server/ledger.go +++ b/internal/server/ledger.go @@ -33,11 +33,11 @@ func GetLedger(db *gorm.DB) gin.H { p.MarketAmount = service.GetMarketPrice(db, p, date) return p }) - breakdowns := computeBreakdown(lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, "Asset:") })) + breakdowns := computeBreakdown(db, lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, "Asset:") })) return gin.H{"postings": postings, "breakdowns": breakdowns} } -func computeBreakdown(postings []posting.Posting) map[string]Breakdown { +func computeBreakdown(db *gorm.DB, postings []posting.Posting) map[string]Breakdown { accounts := make(map[string]bool) for _, p := range postings { var parts []string @@ -53,9 +53,22 @@ func computeBreakdown(postings []posting.Posting) map[string]Breakdown { for group := range accounts { ps := lo.Filter(postings, func(p posting.Posting, _ int) bool { return strings.HasPrefix(p.Account, group) }) - amount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.Amount }, 0.0) + amount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { + if service.IsInterest(db, p) { + return acc + } else { + return acc + p.Amount + } + }, 0.0) marketAmount := lo.Reduce(ps, func(acc float64, p posting.Posting, _ int) float64 { return acc + p.MarketAmount }, 0.0) - payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment { return xirr.Payment{Date: p.Date, Amount: -p.Amount} })) + payments := lo.Reverse(lo.Map(ps, func(p posting.Posting, _ int) xirr.Payment { + if service.IsInterest(db, p) { + return xirr.Payment{Date: p.Date, Amount: 0} + } else { + return xirr.Payment{Date: p.Date, Amount: -p.Amount} + } + + })) payments = append(payments, xirr.Payment{Date: today, Amount: marketAmount}) returns, err := xirr.XIRR(payments) if err != nil { diff --git a/internal/server/overview.go b/internal/server/overview.go index 8e784f3..2b1cee4 100644 --- a/internal/server/overview.go +++ b/internal/server/overview.go @@ -42,11 +42,19 @@ func ComputeTimeline(db *gorm.DB, postings []posting.Posting) []Networth { } actual := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 { - return p.Amount + agg + if service.IsInterest(db, p) { + return agg + } else { + return p.Amount + agg + } }, 0) gain := lo.Reduce(pastPostings, func(agg float64, p posting.Posting, _ int) float64 { - return service.GetMarketPrice(db, p, start) - p.Amount + agg + if service.IsInterest(db, p) { + return p.Amount + agg + } else { + return service.GetMarketPrice(db, p, start) - p.Amount + agg + } }, 0) networths = append(networths, Networth{Date: start, Actual: actual, Gain: gain}) } diff --git a/internal/server/server.go b/internal/server/server.go index 8393e45..2932350 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -26,5 +26,8 @@ func Listen(db *gorm.DB) { c.JSON(200, GetLedger(db)) }) log.Info("Listening on 7500") - router.Run(":7500") + err := router.Run(":7500") + if err != nil { + log.Fatal(err) + } } diff --git a/internal/service/interest.go b/internal/service/interest.go new file mode 100644 index 0000000..b7a8ddb --- /dev/null +++ b/internal/service/interest.go @@ -0,0 +1,55 @@ +package service + +import ( + "sync" + "time" + + "github.com/ananthakumaran/paisa/internal/model/posting" + "github.com/samber/lo" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type interestCache struct { + mu sync.Mutex + loaded bool + postings map[time.Time][]posting.Posting +} + +var icache interestCache + +func loadInterestCache(db *gorm.DB) { + icache.mu.Lock() + defer icache.mu.Unlock() + + if icache.loaded { + return + } + + var postings []posting.Posting + result := db.Where("account like ?", "Income:Interest:%").Find(&postings) + if result.Error != nil { + log.Fatal(result.Error) + } + + icache.postings = lo.GroupBy(postings, func(p posting.Posting) time.Time { return p.Date }) + icache.loaded = true +} + +func IsInterest(db *gorm.DB, p posting.Posting) bool { + if p.Commodity != "INR" { + return false + } + + loadInterestCache(db) + + for _, ip := range icache.postings[p.Date] { + if ip.Date.Equal(p.Date) && + -ip.Amount == p.Amount && + ip.Payee == p.Payee { + return true + } + } + + return false +} diff --git a/internal/service/market.go b/internal/service/market.go index cc56f1b..19f29c6 100644 --- a/internal/service/market.go +++ b/internal/service/market.go @@ -15,16 +15,17 @@ import ( type priceCache struct { mu sync.Mutex + loaded bool pricesTree map[string]*btree.BTree } -var cache priceCache +var pcache priceCache -func loadCache(db *gorm.DB) { - cache.mu.Lock() - defer cache.mu.Unlock() +func loadPriceCache(db *gorm.DB) { + pcache.mu.Lock() + defer pcache.mu.Unlock() - if cache.pricesTree != nil { + if pcache.loaded { return } @@ -33,14 +34,14 @@ func loadCache(db *gorm.DB) { if result.Error != nil { log.Fatal(result.Error) } - cache.pricesTree = make(map[string]*btree.BTree) + pcache.pricesTree = make(map[string]*btree.BTree) for _, price := range prices { - if cache.pricesTree[price.CommodityName] == nil { - cache.pricesTree[price.CommodityName] = btree.New(2) + if pcache.pricesTree[price.CommodityName] == nil { + pcache.pricesTree[price.CommodityName] = btree.New(2) } - cache.pricesTree[price.CommodityName].ReplaceOrInsert(price) + pcache.pricesTree[price.CommodityName].ReplaceOrInsert(price) } var postings []posting.Posting @@ -50,22 +51,24 @@ func loadCache(db *gorm.DB) { } for commodityName, postings := range lo.GroupBy(postings, func(p posting.Posting) string { return p.Commodity }) { - if postings[0].Commodity != "INR" && cache.pricesTree[commodityName] == nil { - cache.pricesTree[commodityName] = btree.New(2) + if postings[0].Commodity != "INR" && pcache.pricesTree[commodityName] == nil { + pcache.pricesTree[commodityName] = btree.New(2) for _, p := range postings { - cache.pricesTree[commodityName].ReplaceOrInsert(price.Price{Date: p.Date, CommodityID: p.Commodity, CommodityName: p.Commodity, Value: p.Amount / p.Quantity}) + pcache.pricesTree[commodityName].ReplaceOrInsert(price.Price{Date: p.Date, CommodityID: p.Commodity, CommodityName: p.Commodity, Value: p.Amount / p.Quantity}) } } } + + pcache.loaded = true } func GetMarketPrice(db *gorm.DB, p posting.Posting, date time.Time) float64 { - loadCache(db) + loadPriceCache(db) if p.Commodity == "INR" { return p.Amount } - pt := cache.pricesTree[p.Commodity] + pt := pcache.pricesTree[p.Commodity] if pt != nil { pc := utils.BTreeDescendFirstLessOrEqual(pt, price.Price{Date: date})