package handler import ( "errors" "strconv" "time" "accounting-app/pkg/api" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // InterestHandler handles HTTP requests for interest operations // Feature: financial-core-upgrade // Validates: Requirements 3.4 type InterestHandler struct { interestService *service.InterestService interestScheduler *service.InterestScheduler } // NewInterestHandler creates a new InterestHandler instance func NewInterestHandler(interestService *service.InterestService, interestScheduler *service.InterestScheduler) *InterestHandler { return &InterestHandler{ interestService: interestService, interestScheduler: interestScheduler, } } // ManualInterestRequest represents the request body for manual interest entry type ManualInterestRequest struct { Amount float64 `json:"amount" binding:"required,gt=0"` Date string `json:"date"` // Optional, defaults to today. Format: YYYY-MM-DD Note string `json:"note"` // Optional note } // AddManualInterest handles POST /api/accounts/:id/interest // Adds a manual interest entry for an account // Validates: Requirements 3.4 func (h *InterestHandler) AddManualInterest(c *gin.Context) { // Get user ID from context userID, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } accountID, err := strconv.ParseUint(c.Param("id"), 10, 32) if err != nil { api.BadRequest(c, "Invalid account ID") return } var req ManualInterestRequest if err := c.ShouldBindJSON(&req); err != nil { api.ValidationError(c, "Invalid request body: "+err.Error()) return } // Parse date if provided, otherwise use today var date time.Time if req.Date != "" { date, err = time.Parse("2006-01-02", req.Date) if err != nil { api.BadRequest(c, "Invalid date format. Use YYYY-MM-DD") return } } else { date = time.Now() } result, err := h.interestService.AddManualInterest(userID.(uint), uint(accountID), req.Amount, date, req.Note) if err != nil { if errors.Is(err, service.ErrAccountNotFound) { api.NotFound(c, "Account not found") return } if errors.Is(err, service.ErrInterestNotEnabled) { api.BadRequest(c, "Interest is not enabled for this account") return } api.InternalError(c, "Failed to add manual interest: "+err.Error()) return } api.Created(c, result) } // CalculateInterestRequest represents the request body for triggering interest calculation type CalculateInterestRequest struct { Date string `json:"date"` // Optional, defaults to today. Format: YYYY-MM-DD } // CalculateAllInterest handles POST /api/interest/calculate // Triggers interest calculation for all enabled accounts // This is typically used for manual trigger or testing func (h *InterestHandler) CalculateAllInterest(c *gin.Context) { // Get user ID from context userID, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req CalculateInterestRequest if err := c.ShouldBindJSON(&req); err != nil { // Allow empty body, will use today's date req = CalculateInterestRequest{} } // Parse date if provided, otherwise use today var date time.Time var err error if req.Date != "" { date, err = time.Parse("2006-01-02", req.Date) if err != nil { api.BadRequest(c, "Invalid date format. Use YYYY-MM-DD") return } } else { date = time.Now() } results, err := h.interestService.CalculateAllInterest(userID.(uint), date) if err != nil { api.InternalError(c, "Failed to calculate interest: "+err.Error()) return } // Calculate summary totalInterest := 0.0 for _, r := range results { totalInterest += r.DailyInterest } api.Success(c, gin.H{ "date": date.Format("2006-01-02"), "accounts_count": len(results), "total_interest": totalInterest, "results": results, }) } // GetSchedulerStatus handles GET /api/interest/scheduler/status // Returns the current status of the interest scheduler func (h *InterestHandler) GetSchedulerStatus(c *gin.Context) { if h.interestScheduler == nil { api.Success(c, gin.H{ "running": false, "message": "Interest scheduler is not configured", "last_execution": nil, }) return } lastExecution := h.interestScheduler.GetLastExecution() var lastExecutionStr *string if !lastExecution.IsZero() { s := lastExecution.Format("2006-01-02 15:04:05") lastExecutionStr = &s } api.Success(c, gin.H{ "running": h.interestScheduler.IsRunning(), "last_execution": lastExecutionStr, }) } // RegisterRoutes registers all interest routes to the given router group func (h *InterestHandler) RegisterRoutes(rg *gin.RouterGroup) { // Manual interest entry for specific account rg.POST("/accounts/:id/interest", h.AddManualInterest) // Interest calculation endpoints interest := rg.Group("/interest") { interest.POST("/calculate", h.CalculateAllInterest) interest.GET("/scheduler/status", h.GetSchedulerStatus) } }