package service import ( "context" "fmt" "log" "sync" "time" ) // InterestScheduler handles scheduled calculation of daily interest for all enabled accounts // Feature: financial-core-upgrade // Validates: Requirements 17.1-17.6 type InterestScheduler struct { interestService *InterestService executionTime time.Duration // Time of day to execute (e.g., 5 minutes after midnight) stopChan chan struct{} mu sync.Mutex running bool lastExecution time.Time } // InterestSchedulerConfig holds configuration for the interest scheduler type InterestSchedulerConfig struct { // ExecutionHour is the hour of day to run (0-23), default 0 ExecutionHour int // ExecutionMinute is the minute of hour to run (0-59), default 5 ExecutionMinute int } // DefaultInterestSchedulerConfig returns the default configuration // Default execution time: 00:05 (5 minutes after midnight) // Validates: Requirements 17.1 func DefaultInterestSchedulerConfig() InterestSchedulerConfig { return InterestSchedulerConfig{ ExecutionHour: 0, ExecutionMinute: 5, } } // NewInterestScheduler creates a new InterestScheduler instance // Validates: Requirements 17.1 func NewInterestScheduler(interestService *InterestService, config InterestSchedulerConfig) *InterestScheduler { // Calculate execution time as duration from midnight executionTime := time.Duration(config.ExecutionHour)*time.Hour + time.Duration(config.ExecutionMinute)*time.Minute return &InterestScheduler{ interestService: interestService, executionTime: executionTime, stopChan: make(chan struct{}), running: false, } } // Start begins the scheduled interest calculation // It checks for missed calculations on startup, then runs daily at the configured time // This method blocks until Stop() is called or context is cancelled // Validates: Requirements 17.1, 17.6 func (s *InterestScheduler) Start(ctx context.Context) { s.mu.Lock() if s.running { s.mu.Unlock() log.Println("[InterestScheduler] Scheduler is already running") return } s.running = true s.stopChan = make(chan struct{}) // Reset stop channel s.mu.Unlock() log.Printf("[InterestScheduler] Starting interest scheduler, execution time: %02d:%02d", int(s.executionTime.Hours()), int(s.executionTime.Minutes())%60) // Check for missed calculations on startup (Requirement 17.6) s.checkMissedCalculations(ctx) // Calculate time until next execution nextExecution := s.calculateNextExecution() log.Printf("[InterestScheduler] Next scheduled execution: %s", nextExecution.Format("2006-01-02 15:04:05")) timer := time.NewTimer(time.Until(nextExecution)) defer timer.Stop() for { select { case <-timer.C: s.executeInterestCalculation(ctx) // Reset timer for next day nextExecution = s.calculateNextExecution() log.Printf("[InterestScheduler] Next scheduled execution: %s", nextExecution.Format("2006-01-02 15:04:05")) timer.Reset(time.Until(nextExecution)) case <-s.stopChan: log.Println("[InterestScheduler] Scheduler stopped by Stop() call") s.mu.Lock() s.running = false s.mu.Unlock() return case <-ctx.Done(): log.Println("[InterestScheduler] Scheduler stopped due to context cancellation") s.mu.Lock() s.running = false s.mu.Unlock() return } } } // Stop gracefully stops the scheduler func (s *InterestScheduler) Stop() { s.mu.Lock() defer s.mu.Unlock() if !s.running { log.Println("[InterestScheduler] Scheduler is not running") return } log.Println("[InterestScheduler] Stopping scheduler...") close(s.stopChan) } // IsRunning returns whether the scheduler is currently running func (s *InterestScheduler) IsRunning() bool { s.mu.Lock() defer s.mu.Unlock() return s.running } // GetLastExecution returns the time of the last execution func (s *InterestScheduler) GetLastExecution() time.Time { s.mu.Lock() defer s.mu.Unlock() return s.lastExecution } // calculateNextExecution calculates the next execution time func (s *InterestScheduler) calculateNextExecution() time.Time { now := time.Now() // Calculate today's execution time todayExecution := time.Date( now.Year(), now.Month(), now.Day(), int(s.executionTime.Hours()), int(s.executionTime.Minutes())%60, 0, 0, now.Location(), ) // If today's execution time has passed, schedule for tomorrow if now.After(todayExecution) { return todayExecution.Add(24 * time.Hour) } return todayExecution } // checkMissedCalculations checks for and processes any missed interest calculations // This is called on startup to ensure no interest calculations are missed // Validates: Requirements 17.6 func (s *InterestScheduler) checkMissedCalculations(ctx context.Context) { log.Println("[InterestScheduler] Checking for missed interest calculations...") // Get all interest-enabled accounts accounts, err := s.interestService.GetInterestEnabledAccounts() if err != nil { log.Printf("[InterestScheduler] Error getting interest-enabled accounts: %v", err) return } if len(accounts) == 0 { log.Println("[InterestScheduler] No interest-enabled accounts found") return } // Check yesterday's date (the most recent date that should have been calculated) yesterday := time.Now().AddDate(0, 0, -1) yesterday = time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location()) missedCount := 0 processedCount := 0 for _, account := range accounts { select { case <-ctx.Done(): log.Println("[InterestScheduler] Missed calculation check cancelled") return default: } // Check if interest was calculated for yesterday calculated, err := s.interestService.IsInterestCalculated(account.ID, yesterday) if err != nil { log.Printf("[InterestScheduler] Error checking interest calculation for account %d: %v", account.ID, err) continue } if !calculated { missedCount++ log.Printf("[InterestScheduler] Processing missed interest calculation for account %d (%s) for date %s", account.ID, account.Name, yesterday.Format("2006-01-02")) result, err := s.interestService.CalculateDailyInterest(account.UserID, account.ID, yesterday) if err != nil { log.Printf("[InterestScheduler] Error calculating missed interest for account %d: %v", account.ID, err) continue } if result != nil && result.DailyInterest > 0 { processedCount++ log.Printf("[InterestScheduler] Processed missed interest for account %d: %.2f", account.ID, result.DailyInterest) } } } if missedCount > 0 { log.Printf("[InterestScheduler] Missed calculation check complete: %d missed, %d processed", missedCount, processedCount) } else { log.Println("[InterestScheduler] No missed calculations found") } } // executeInterestCalculation executes the daily interest calculation for all enabled accounts // Validates: Requirements 17.2, 17.3, 17.4 func (s *InterestScheduler) executeInterestCalculation(ctx context.Context) { startTime := time.Now() log.Printf("[InterestScheduler] Starting daily interest calculation at %s", startTime.Format("2006-01-02 15:04:05")) // Use today's date for calculation calculationDate := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, startTime.Location()) // Get all interest-enabled accounts accounts, err := s.interestService.GetInterestEnabledAccounts() if err != nil { log.Printf("[InterestScheduler] Error getting interest-enabled accounts: %v", err) return } totalAccounts := len(accounts) successCount := 0 skipCount := 0 errorCount := 0 totalInterest := 0.0 log.Printf("[InterestScheduler] Processing %d interest-enabled accounts", totalAccounts) // Process each account independently (Requirement 17.4) for _, account := range accounts { select { case <-ctx.Done(): log.Println("[InterestScheduler] Interest calculation cancelled") return default: } result, err := s.interestService.CalculateDailyInterest(account.UserID, account.ID, calculationDate) if err != nil { // Log error but continue with other accounts (Requirement 17.4) errorCount++ log.Printf("[InterestScheduler] Error calculating interest for account %d (%s): %v", account.ID, account.Name, err) continue } if result == nil { skipCount++ continue } if result.DailyInterest > 0 { successCount++ totalInterest += result.DailyInterest log.Printf("[InterestScheduler] Account %d (%s): balance=%.2f, rate=%.4f, interest=%.2f", result.AccountID, result.AccountName, result.Balance, result.AnnualRate, result.DailyInterest) } else { skipCount++ } } // Update last execution time s.mu.Lock() s.lastExecution = startTime s.mu.Unlock() // Log execution summary (Requirement 17.3) endTime := time.Now() duration := endTime.Sub(startTime) log.Printf("[InterestScheduler] Daily interest calculation completed:") log.Printf("[InterestScheduler] Start time: %s", startTime.Format("2006-01-02 15:04:05")) log.Printf("[InterestScheduler] End time: %s", endTime.Format("2006-01-02 15:04:05")) log.Printf("[InterestScheduler] Duration: %v", duration) log.Printf("[InterestScheduler] Total accounts: %d", totalAccounts) log.Printf("[InterestScheduler] Successful: %d", successCount) log.Printf("[InterestScheduler] Skipped: %d", skipCount) log.Printf("[InterestScheduler] Errors: %d", errorCount) log.Printf("[InterestScheduler] Total interest: %.2f", totalInterest) } // ForceCalculation triggers an immediate interest calculation for a specific user outside of the regular schedule // This can be used for manual trigger or testing func (s *InterestScheduler) ForceCalculation(ctx context.Context, userID uint) (*InterestCalculationSummary, error) { log.Printf("[InterestScheduler] Force calculation triggered for user %d", userID) startTime := time.Now() calculationDate := time.Date(startTime.Year(), startTime.Month(), startTime.Day(), 0, 0, 0, 0, startTime.Location()) results, err := s.interestService.CalculateAllInterest(userID, calculationDate) if err != nil { return nil, fmt.Errorf("failed to calculate interest: %w", err) } endTime := time.Now() summary := &InterestCalculationSummary{ StartTime: startTime, EndTime: endTime, Duration: endTime.Sub(startTime), AccountsCount: len(results), Results: results, } // Calculate total interest for _, r := range results { summary.TotalInterest += r.DailyInterest } return summary, nil } // InterestCalculationSummary represents the summary of an interest calculation run type InterestCalculationSummary struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Duration time.Duration `json:"duration"` AccountsCount int `json:"accounts_count"` TotalInterest float64 `json:"total_interest"` Results []InterestResult `json:"results"` }