Files
Novault-backend/internal/handler/interest_handler.go
2026-01-25 21:59:00 +08:00

181 lines
4.9 KiB
Go

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)
}
}