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:
Cosmo
2026-02-16 06:48:09 +00:00
parent 9e90aa6d95
commit 2a50e50771
18 changed files with 2910 additions and 162 deletions

View File

@@ -13,20 +13,22 @@ import (
)
type Scheduler struct {
cron *cron.Cron
bot *bot.Bot
userRepo *repository.UserRepository
taskRepo *repository.TaskRepository
habitRepo *repository.HabitRepository
cron *cron.Cron
bot *bot.Bot
userRepo *repository.UserRepository
taskRepo *repository.TaskRepository
habitRepo *repository.HabitRepository
freezeRepo *repository.HabitFreezeRepository
}
func New(b *bot.Bot, userRepo *repository.UserRepository, taskRepo *repository.TaskRepository, habitRepo *repository.HabitRepository) *Scheduler {
func New(b *bot.Bot, userRepo *repository.UserRepository, taskRepo *repository.TaskRepository, habitRepo *repository.HabitRepository, freezeRepo *repository.HabitFreezeRepository) *Scheduler {
return &Scheduler{
cron: cron.New(),
bot: b,
userRepo: userRepo,
taskRepo: taskRepo,
habitRepo: habitRepo,
cron: cron.New(),
bot: b,
userRepo: userRepo,
taskRepo: taskRepo,
habitRepo: habitRepo,
freezeRepo: freezeRepo,
}
}
@@ -117,19 +119,13 @@ func (s *Scheduler) sendMorningBriefing(userID, chatID int64, loc *time.Location
return
}
// Filter habits for today
// Filter habits for today (excluding frozen ones)
weekday := int(time.Now().In(loc).Weekday())
today := time.Now().In(loc).Truncate(24 * time.Hour)
var todayHabits int
for _, habit := range habits {
if habit.Frequency == "daily" {
if s.shouldShowHabitToday(habit, userID, weekday, today) {
todayHabits++
} else {
for _, day := range habit.TargetDays {
if day == weekday {
todayHabits++
break
}
}
}
}
@@ -185,25 +181,14 @@ func (s *Scheduler) sendEveningSummary(userID, chatID int64, loc *time.Location)
return
}
// Filter and count today's habits
// Filter and count today's habits (excluding frozen ones)
weekday := int(time.Now().In(loc).Weekday())
today := time.Now().In(loc).Truncate(24 * time.Hour)
var completedHabits, incompleteHabits int
var incompleteHabitNames []string
for _, habit := range habits {
isToday := false
if habit.Frequency == "daily" {
isToday = true
} else {
for _, day := range habit.TargetDays {
if day == weekday {
isToday = true
break
}
}
}
if isToday {
if s.shouldShowHabitToday(habit, userID, weekday, today) {
completed, _ := s.habitRepo.IsHabitCompletedToday(habit.ID, userID)
if completed {
completedHabits++
@@ -313,17 +298,37 @@ func (s *Scheduler) checkHabitReminders(userID, chatID int64, currentTime string
return
}
today := time.Now().Truncate(24 * time.Hour)
for _, habit := range habits {
if habit.UserID != userID {
continue
}
// Check if habit is frozen today
frozen, err := s.freezeRepo.IsHabitFrozenOnDate(habit.ID, today)
if err != nil {
log.Printf("Scheduler: error checking freeze for habit %d: %v", habit.ID, err)
continue
}
if frozen {
continue
}
// Check if already completed today
completed, _ := s.habitRepo.IsHabitCompletedToday(habit.ID, userID)
if completed {
continue
}
// For interval habits, check if it should be shown today
if (habit.Frequency == "interval" || habit.Frequency == "custom") && habit.TargetCount > 0 {
shouldShow, err := s.habitRepo.ShouldShowIntervalHabitToday(habit.ID, userID, habit.TargetCount, habit.StartDate)
if err != nil || !shouldShow {
continue
}
}
text := fmt.Sprintf("⏰ <b>Напоминание о привычке:</b>\n\n%s <b>%s</b>", habit.Icon, habit.Name)
if habit.Description != "" {
text += fmt.Sprintf("\n<i>%s</i>", habit.Description)
@@ -339,3 +344,40 @@ func (s *Scheduler) checkHabitReminders(userID, chatID int64, currentTime string
s.bot.SendMessageWithKeyboard(chatID, text, &keyboard)
}
}
// shouldShowHabitToday checks if a habit should be shown today based on its frequency and freeze status
func (s *Scheduler) shouldShowHabitToday(habit model.Habit, userID int64, weekday int, today time.Time) bool {
// Check if habit is frozen today
frozen, err := s.freezeRepo.IsHabitFrozenOnDate(habit.ID, today)
if err != nil {
log.Printf("Scheduler: error checking freeze for habit %d: %v", habit.ID, err)
return false
}
if frozen {
return false
}
if habit.Frequency == "daily" {
return true
}
if habit.Frequency == "weekly" {
for _, day := range habit.TargetDays {
if day == weekday {
return true
}
}
return false
}
// For interval habits
if (habit.Frequency == "interval" || habit.Frequency == "custom") && habit.TargetCount > 0 {
shouldShow, err := s.habitRepo.ShouldShowIntervalHabitToday(habit.ID, userID, habit.TargetCount, habit.StartDate)
if err != nil {
return false
}
return shouldShow
}
return true
}