Skip to content

Commit

Permalink
WIP: credit card
Browse files Browse the repository at this point in the history
  • Loading branch information
ananthakumaran committed Jan 25, 2024
1 parent 544d696 commit 3339a09
Show file tree
Hide file tree
Showing 19 changed files with 957 additions and 59 deletions.
10 changes: 10 additions & 0 deletions internal/accounting/accounting.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,13 @@ func GroupByAccount(posts []posting.Posting) map[string][]posting.Posting {
return post.Account
})
}

func GroupByMonthlyBillingCycle(postsings []posting.Posting, billDate int) map[string][]posting.Posting {
return lo.GroupBy(postsings, func(p posting.Posting) string {
if p.Date.Day() >= billDate {
return utils.BeginningOfMonth(p.Date).AddDate(0, 1, 0).Format("2006-01")
} else {
return p.Date.Format("2006-01")
}
})
}
12 changes: 12 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,15 @@ type AllocationTarget struct {
Accounts []string `json:"accounts" yaml:"accounts"`
}

type CreditCard struct {
Account string `json:"account" yaml:"account"`
CreditLimit int `json:"credit_limit" yaml:"credit_limit"`
StatementEndDay int `json:"statement_end_day" yaml:"statement_end_day"`
DueDay int `json:"due_day" yaml:"due_day"`
Network string `json:"network" yaml:"network"`
Number string `json:"number" yaml:"number"`
}

type Config struct {
JournalPath string `json:"journal_path" yaml:"journal_path"`
DBPath string `json:"db_path" yaml:"db_path"`
Expand Down Expand Up @@ -143,6 +152,8 @@ type Config struct {
Goals Goals `json:"goals" yaml:"goals"`

UserAccounts []UserAccount `json:"user_accounts" yaml:"user_accounts"`

CreditCards []CreditCard `json:"credit_cards" yaml:"credit_cards"`
}

var config Config
Expand All @@ -166,6 +177,7 @@ var defaultConfig = Config{
Accounts: []Account{},
Goals: Goals{Retirement: []RetirementGoal{}, Savings: []SavingsGoal{}},
UserAccounts: []UserAccount{},
CreditCards: []CreditCard{},
}

var itemsUniquePropertiesMeta = jsonschema.MustCompileString("itemsUniqueProperties.json", `{
Expand Down
66 changes: 63 additions & 3 deletions internal/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@
"properties": {
"name": {
"type": "string",
"description": "name of the commodity"
"description": "Name of the commodity"
},
"type": {
"type": "string",
Expand Down Expand Up @@ -401,7 +401,7 @@
"properties": {
"name": {
"type": "string",
"description": "name of the template",
"description": "Name of the template",
"minLength": 1
},
"content": {
Expand All @@ -428,7 +428,7 @@
"properties": {
"name": {
"type": "string",
"description": "name of the account",
"description": "Name of the account",
"minLength": 1
},
"icon": {
Expand All @@ -440,6 +440,66 @@
"required": ["name"],
"additionalProperties": false
}
},
"credit_cards": {
"type": "array",
"itemsUniqueProperties": ["account"],
"default": [
{
"account": "Liabilities:CreditCard:Chase",
"credit_limit": 100000,
"statement_end_day": 28,
"due_day": 15
}
],
"items": {
"type": "object",
"ui:header": "account",
"properties": {
"account": {
"type": "string",
"description": "Name of the credit card account"
},
"credit_limit": {
"type": "number",
"description": "Credit limit of the card",
"minimum": 1
},
"statement_end_day": {
"type": "integer",
"description": "Statement end day of the card",
"minimum": 1,
"maximum": 31
},
"due_day": {
"type": "integer",
"description": "Due day of the card",
"minimum": 1,
"maximum": 31
},
"network": {
"type": "string",
"description": "Network of the card",
"enum": ["visa", "mastercard", "dinersclub", "amex", "rupay", "jcb", "discover"]
},
"number": {
"type": "string",
"description": "Last 4 digits of the card number",
"maxLength": 4,
"minLength": 4,
"pattern": "^[0-9]{4}$"
}
},
"required": [
"account",
"credit_limit",
"statement_end_day",
"due_day",
"network",
"number"
],
"additionalProperties": false
}
}
},
"required": ["journal_path", "db_path"],
Expand Down
131 changes: 131 additions & 0 deletions internal/server/credit_card.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package server

import (
"time"

"github.com/ananthakumaran/paisa/internal/accounting"
"github.com/ananthakumaran/paisa/internal/config"
"github.com/ananthakumaran/paisa/internal/model/posting"
"github.com/ananthakumaran/paisa/internal/query"
"github.com/ananthakumaran/paisa/internal/utils"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
log "github.com/sirupsen/logrus"
"gorm.io/gorm"
)

type CreditCardSummary struct {
Account string `json:"account"`
Network string `json:"network"`
Number string `json:"number"`
Balance decimal.Decimal `json:"balance"`
Bills []CreditCardBill `json:"bills"`
CreditLimit decimal.Decimal `json:"creditLimit"`
}

type CreditCardBill struct {
StatementStartDate time.Time `json:"statementStartDate"`
StatementEndDate time.Time `json:"statementEndDate"`
DueDate time.Time `json:"dueDate"`
PaidDate *time.Time `json:"paidDate"`
Credits decimal.Decimal `json:"credits"`
Debits decimal.Decimal `json:"debits"`
OpeningBalance decimal.Decimal `json:"openingBalance"`
ClosingBalance decimal.Decimal `json:"closingBalance"`
Postings []posting.Posting `json:"postings"`
}

func GetCreditCards(db *gorm.DB) gin.H {
creditCards := []CreditCardSummary{}

for _, creditCardConfig := range config.GetConfig().CreditCards {
ps := query.Init(db).Where("account = ?", creditCardConfig.Account).All()
creditCards = append(creditCards, buildCreditCard(creditCardConfig, ps))
}

return gin.H{"creditCards": creditCards}
}

func buildCreditCard(creditCardConfig config.CreditCard, ps []posting.Posting) CreditCardSummary {
bills := computeBills(creditCardConfig, ps)
balance := decimal.Zero
if len(bills) > 0 {
balance = bills[len(bills)-1].ClosingBalance
}
return CreditCardSummary{
Account: creditCardConfig.Account,
Network: creditCardConfig.Network,
Number: creditCardConfig.Number,
Balance: balance,
Bills: bills,
CreditLimit: decimal.NewFromInt(int64(creditCardConfig.CreditLimit)),
}
}

func computeBills(creditCardConfig config.CreditCard, ps []posting.Posting) []CreditCardBill {
bills := []CreditCardBill{}

grouped := accounting.GroupByMonthlyBillingCycle(ps, creditCardConfig.StatementEndDay)

balance := decimal.Zero
unpaidBalance := decimal.Zero
unpaidBill := 0

for _, month := range utils.SortedKeys(grouped) {
statementEndDate, err := time.Parse("2006-01", month)
if err != nil {
log.Fatal(err)
}

statementEndDate = statementEndDate.AddDate(0, 0, creditCardConfig.StatementEndDay-1)
statementStartDate := statementEndDate.AddDate(0, -1, -1)

var dueDate time.Time
if creditCardConfig.StatementEndDay < creditCardConfig.DueDay {
dueDate = utils.BeginningOfMonth(statementEndDate).AddDate(0, 0, creditCardConfig.DueDay-1)
} else {
dueDate = utils.BeginningOfMonth(statementEndDate).AddDate(0, 1, creditCardConfig.DueDay-1)
}

bill := CreditCardBill{
StatementStartDate: statementStartDate,
StatementEndDate: statementEndDate,
DueDate: dueDate,
OpeningBalance: balance,
Postings: []posting.Posting{},
}

for _, p := range grouped[month] {
balance = balance.Add(p.Amount.Neg())

if p.Amount.IsPositive() {
bill.Credits = bill.Credits.Add(p.Amount)
if unpaidBalance.IsPositive() {

unpaidBalance = unpaidBalance.Sub(p.Amount)
if unpaidBalance.LessThanOrEqual(decimal.Zero) {

unpaidBalance = decimal.Zero
for i := unpaidBill; i < len(bills); i++ {
paidDate := p.Date
bills[i].PaidDate = &paidDate
}
unpaidBill = len(bills)
}

}
} else {
bill.Debits = bill.Debits.Add(p.Amount.Neg())
}

bill.Postings = append(bill.Postings, p)

}

bill.ClosingBalance = balance
unpaidBalance = balance
bills = append(bills, bill)
}

return bills
}
4 changes: 4 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,10 @@ func Build(db *gorm.DB, enableCompression bool) *gin.Engine {
c.JSON(200, goal.GetGoalDetails(db, c.Param("type"), c.Param("name")))
})

router.GET("/api/credit_cards", func(c *gin.Context) {
c.JSON(200, GetCreditCards(db))
})

router.NoRoute(func(c *gin.Context) {
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(web.Index))
})
Expand Down
30 changes: 30 additions & 0 deletions src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1088,3 +1088,33 @@ textarea:invalid {
div.is-hoverable:hover {
background-color: $white-bis;
}

// credit card

.credit-card-container {
display: grid;
gap: 18px;
grid-template-columns: repeat(auto-fill, minmax(19rem, 25rem));
}

.credit-card {
aspect-ratio: 3.375/2.125;
border-radius: 0.7rem;
display: flex;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.3) !important;
background: linear-gradient(
345deg,
$grey-lightest 0%,
$grey-lightest 60%,
$grey-lighter 60%,
$grey-lighter 85%,
$grey-light 85%,
$grey-light 95%,
$grey 95%,
$grey 100%
);

.chip {
color: $amber-700;
}
}
15 changes: 15 additions & 0 deletions src/dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,19 @@ html[data-theme="dark"] {
}
}
}

.credit-card {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 1) !important;
background: linear-gradient(
345deg,
$white 0%,
$white 60%,
$white-bis 60%,
$white-bis 85%,
$white-ter 85%,
$white-ter 95%,
$grey-lightest 95%,
$grey-lightest 100%
);
}
}
Loading

0 comments on commit 3339a09

Please sign in to comment.