feat(savings): Add savings module with categories, transactions, recurring plans
- Categories: regular, deposits, credits, recurring, multi-user, accounts - Transactions: deposits and withdrawals with user tracking - Recurring plans: monthly payment obligations per user - Stats: overdues calculation with allocation algorithm - Excludes is_account categories from total sums - Documentation: docs/SAVINGS.md
This commit is contained in:
228
internal/model/savings.go
Normal file
228
internal/model/savings.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SavingsCategory struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
UserID int64 `db:"user_id" json:"user_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
|
||||
// Type flags
|
||||
IsDeposit bool `db:"is_deposit" json:"is_deposit"`
|
||||
IsCredit bool `db:"is_credit" json:"is_credit"`
|
||||
IsAccount bool `db:"is_account" json:"is_account"`
|
||||
IsRecurring bool `db:"is_recurring" json:"is_recurring"`
|
||||
IsMulti bool `db:"is_multi" json:"is_multi"`
|
||||
IsClosed bool `db:"is_closed" json:"is_closed"`
|
||||
|
||||
// Initial capital
|
||||
InitialCapital float64 `db:"initial_capital" json:"initial_capital"`
|
||||
|
||||
// Deposit fields
|
||||
DepositAmount float64 `db:"deposit_amount" json:"deposit_amount"`
|
||||
InterestRate float64 `db:"interest_rate" json:"interest_rate"`
|
||||
DepositStartDate sql.NullTime `db:"deposit_start_date" json:"-"`
|
||||
DepositStartStr *string `db:"-" json:"deposit_start_date"`
|
||||
DepositTerm int `db:"deposit_term" json:"deposit_term"`
|
||||
DepositEndDate sql.NullTime `db:"deposit_end_date" json:"-"`
|
||||
DepositEndStr *string `db:"-" json:"deposit_end_date"`
|
||||
LastInterestCalc sql.NullTime `db:"last_interest_calc" json:"-"`
|
||||
FinalAmount float64 `db:"final_amount" json:"final_amount"`
|
||||
|
||||
// Credit fields
|
||||
CreditAmount float64 `db:"credit_amount" json:"credit_amount"`
|
||||
CreditTerm int `db:"credit_term" json:"credit_term"`
|
||||
CreditRate float64 `db:"credit_rate" json:"credit_rate"`
|
||||
CreditStartDate sql.NullTime `db:"credit_start_date" json:"-"`
|
||||
CreditStartStr *string `db:"-" json:"credit_start_date"`
|
||||
|
||||
// Recurring fields
|
||||
RecurringAmount float64 `db:"recurring_amount" json:"recurring_amount"`
|
||||
RecurringDay int `db:"recurring_day" json:"recurring_day"`
|
||||
RecurringStartDate sql.NullTime `db:"recurring_start_date" json:"-"`
|
||||
LastRecurringRun sql.NullTime `db:"last_recurring_run" json:"-"`
|
||||
|
||||
// Computed (populated in service)
|
||||
CurrentAmount float64 `db:"-" json:"current_amount"`
|
||||
RecurringTotalAmount float64 `db:"-" json:"recurring_total_amount"`
|
||||
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
|
||||
// Relations
|
||||
Members []SavingsCategoryMember `db:"-" json:"members,omitempty"`
|
||||
}
|
||||
|
||||
func (c *SavingsCategory) ProcessForJSON() {
|
||||
if c.DepositStartDate.Valid {
|
||||
formatted := c.DepositStartDate.Time.Format("2006-01-02")
|
||||
c.DepositStartStr = &formatted
|
||||
}
|
||||
if c.DepositEndDate.Valid {
|
||||
formatted := c.DepositEndDate.Time.Format("2006-01-02")
|
||||
c.DepositEndStr = &formatted
|
||||
}
|
||||
if c.CreditStartDate.Valid {
|
||||
formatted := c.CreditStartDate.Time.Format("2006-01-02")
|
||||
c.CreditStartStr = &formatted
|
||||
}
|
||||
}
|
||||
|
||||
type SavingsTransaction struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
CategoryID int64 `db:"category_id" json:"category_id"`
|
||||
UserID int64 `db:"user_id" json:"user_id"`
|
||||
Amount float64 `db:"amount" json:"amount"`
|
||||
Type string `db:"type" json:"type"` // deposit, withdrawal
|
||||
Description string `db:"description" json:"description"`
|
||||
Date time.Time `db:"date" json:"date"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
CategoryName string `db:"category_name" json:"category_name,omitempty"`
|
||||
UserName string `db:"user_name" json:"user_name,omitempty"`
|
||||
}
|
||||
|
||||
type SavingsRecurringPlan struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
CategoryID int64 `db:"category_id" json:"category_id"`
|
||||
UserID sql.NullInt64 `db:"user_id" json:"-"`
|
||||
UserIDPtr *int64 `db:"-" json:"user_id"`
|
||||
Effective time.Time `db:"effective" json:"effective"`
|
||||
Amount float64 `db:"amount" json:"amount"`
|
||||
Day int `db:"day" json:"day"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (p *SavingsRecurringPlan) ProcessForJSON() {
|
||||
if p.UserID.Valid {
|
||||
p.UserIDPtr = &p.UserID.Int64
|
||||
}
|
||||
}
|
||||
|
||||
type SavingsCategoryMember struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
CategoryID int64 `db:"category_id" json:"category_id"`
|
||||
UserID int64 `db:"user_id" json:"user_id"`
|
||||
UserName string `db:"user_name" json:"user_name,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// Request DTOs
|
||||
type CreateSavingsCategoryRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
IsDeposit bool `json:"is_deposit,omitempty"`
|
||||
IsCredit bool `json:"is_credit,omitempty"`
|
||||
IsAccount bool `json:"is_account,omitempty"`
|
||||
IsRecurring bool `json:"is_recurring,omitempty"`
|
||||
IsMulti bool `json:"is_multi,omitempty"`
|
||||
InitialCapital float64 `json:"initial_capital,omitempty"`
|
||||
DepositAmount float64 `json:"deposit_amount,omitempty"`
|
||||
InterestRate float64 `json:"interest_rate,omitempty"`
|
||||
DepositStartDate *string `json:"deposit_start_date,omitempty"`
|
||||
DepositTerm int `json:"deposit_term,omitempty"`
|
||||
CreditAmount float64 `json:"credit_amount,omitempty"`
|
||||
CreditTerm int `json:"credit_term,omitempty"`
|
||||
CreditRate float64 `json:"credit_rate,omitempty"`
|
||||
CreditStartDate *string `json:"credit_start_date,omitempty"`
|
||||
RecurringAmount float64 `json:"recurring_amount,omitempty"`
|
||||
RecurringDay int `json:"recurring_day,omitempty"`
|
||||
RecurringStartDate *string `json:"recurring_start_date,omitempty"`
|
||||
MemberIDs []int64 `json:"member_ids,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSavingsCategoryRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
IsDeposit *bool `json:"is_deposit,omitempty"`
|
||||
IsCredit *bool `json:"is_credit,omitempty"`
|
||||
IsAccount *bool `json:"is_account,omitempty"`
|
||||
IsRecurring *bool `json:"is_recurring,omitempty"`
|
||||
IsMulti *bool `json:"is_multi,omitempty"`
|
||||
IsClosed *bool `json:"is_closed,omitempty"`
|
||||
InitialCapital *float64 `json:"initial_capital,omitempty"`
|
||||
DepositAmount *float64 `json:"deposit_amount,omitempty"`
|
||||
InterestRate *float64 `json:"interest_rate,omitempty"`
|
||||
DepositStartDate *string `json:"deposit_start_date,omitempty"`
|
||||
DepositTerm *int `json:"deposit_term,omitempty"`
|
||||
FinalAmount *float64 `json:"final_amount,omitempty"`
|
||||
CreditAmount *float64 `json:"credit_amount,omitempty"`
|
||||
CreditTerm *int `json:"credit_term,omitempty"`
|
||||
CreditRate *float64 `json:"credit_rate,omitempty"`
|
||||
CreditStartDate *string `json:"credit_start_date,omitempty"`
|
||||
RecurringAmount *float64 `json:"recurring_amount,omitempty"`
|
||||
RecurringDay *int `json:"recurring_day,omitempty"`
|
||||
RecurringStartDate *string `json:"recurring_start_date,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSavingsTransactionRequest struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Type string `json:"type"` // deposit, withdrawal
|
||||
Description string `json:"description,omitempty"`
|
||||
Date string `json:"date"`
|
||||
}
|
||||
|
||||
type UpdateSavingsTransactionRequest struct {
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Date *string `json:"date,omitempty"`
|
||||
}
|
||||
|
||||
type CreateRecurringPlanRequest struct {
|
||||
Effective string `json:"effective"`
|
||||
Amount float64 `json:"amount"`
|
||||
Day int `json:"day,omitempty"`
|
||||
UserID *int64 `json:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateRecurringPlanRequest struct {
|
||||
Effective *string `json:"effective,omitempty"`
|
||||
Amount *float64 `json:"amount,omitempty"`
|
||||
Day *int `json:"day,omitempty"`
|
||||
}
|
||||
|
||||
type SavingsStats struct {
|
||||
MonthlyPayments float64 `json:"monthly_payments"`
|
||||
MonthlyPaymentDetails []MonthlyPaymentDetail `json:"monthly_payment_details"`
|
||||
Overdues []OverduePayment `json:"overdues"`
|
||||
TotalBalance float64 `json:"total_balance"`
|
||||
TotalDeposits float64 `json:"total_deposits"`
|
||||
TotalWithdrawals float64 `json:"total_withdrawals"`
|
||||
CategoriesCount int `json:"categories_count"`
|
||||
ByCategory []CategoryStats `json:"by_category"`
|
||||
}
|
||||
|
||||
type CategoryStats struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Balance float64 `json:"balance"`
|
||||
IsDeposit bool `json:"is_deposit"`
|
||||
IsRecurring bool `json:"is_recurring"`
|
||||
}
|
||||
|
||||
// MonthlyPaymentDetail represents a recurring payment detail
|
||||
type MonthlyPaymentDetail struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
Day int `json:"day"`
|
||||
}
|
||||
|
||||
// OverduePayment represents an overdue recurring payment
|
||||
type OverduePayment struct {
|
||||
CategoryID int64 `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
UserID int64 `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Amount float64 `json:"amount"`
|
||||
DueDay int `json:"due_day"`
|
||||
DaysOverdue int `json:"days_overdue"`
|
||||
Month string `json:"month"`
|
||||
}
|
||||
Reference in New Issue
Block a user