Files
pulse-api/internal/model/savings.go
Cosmo 2a50e50771 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
2026-02-16 06:48:09 +00:00

229 lines
9.2 KiB
Go

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"`
}