package handler import ( "fmt" "time" "accounting-app/pkg/api" "accounting-app/internal/models" "accounting-app/internal/service" "github.com/gin-gonic/gin" ) // ReportHandler handles HTTP requests for reports type ReportHandler struct { reportService *service.ReportService pdfExportService *service.PDFExportService excelExportService *service.ExcelExportService } // NewReportHandler creates a new ReportHandler instance func NewReportHandler(reportService *service.ReportService, pdfExportService *service.PDFExportService, excelExportService *service.ExcelExportService) *ReportHandler { return &ReportHandler{ reportService: reportService, pdfExportService: pdfExportService, excelExportService: excelExportService, } } // GetTransactionSummaryRequest represents the request for transaction summary type GetTransactionSummaryRequest struct { StartDate string `form:"start_date" binding:"required"` EndDate string `form:"end_date" binding:"required"` TargetCurrency *string `form:"target_currency"` ConversionDate *string `form:"conversion_date"` } // GetCategorySummaryRequest represents the request for category summary type GetCategorySummaryRequest struct { StartDate string `form:"start_date" binding:"required"` EndDate string `form:"end_date" binding:"required"` TransactionType string `form:"type" binding:"required,oneof=income expense"` TargetCurrency *string `form:"target_currency"` ConversionDate *string `form:"conversion_date"` } // GetTrendDataRequest represents the request for trend data type GetTrendDataRequest struct { StartDate string `form:"start_date" binding:"required"` EndDate string `form:"end_date" binding:"required"` Period string `form:"period" binding:"required,oneof=day week month year"` Currency *string `form:"currency"` } // GetComparisonDataRequest represents the request for comparison data type GetComparisonDataRequest struct { StartDate string `form:"start_date" binding:"required"` EndDate string `form:"end_date" binding:"required"` Currency *string `form:"currency"` } // GetAssetsSummaryRequest represents the request for assets summary type GetAssetsSummaryRequest struct { TargetCurrency *string `form:"target_currency"` ConversionDate *string `form:"conversion_date"` } // GetTransactionSummary handles GET /api/v1/reports/summary func (h *ReportHandler) GetTransactionSummary(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetTransactionSummaryRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse optional target currency var targetCurrency *models.Currency if req.TargetCurrency != nil && *req.TargetCurrency != "" { currency := models.Currency(*req.TargetCurrency) // Validate currency if !isValidCurrency(currency) { api.BadRequest(c, "Invalid target_currency") return } targetCurrency = ¤cy } // Parse optional conversion date var conversionDate *time.Time if req.ConversionDate != nil && *req.ConversionDate != "" { date, err := time.Parse("2006-01-02", *req.ConversionDate) if err != nil { api.BadRequest(c, "Invalid conversion_date format, expected YYYY-MM-DD") return } conversionDate = &date } // Get summary summary, err := h.reportService.GetTransactionSummary(userId.(uint), startDate, endDate, targetCurrency, conversionDate) if err != nil { api.InternalError(c, "Failed to get transaction summary: "+err.Error()) return } api.Success(c, summary) } // GetCategorySummary handles GET /api/v1/reports/category func (h *ReportHandler) GetCategorySummary(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetCategorySummaryRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse transaction type var transactionType models.TransactionType switch req.TransactionType { case "income": transactionType = models.TransactionTypeIncome case "expense": transactionType = models.TransactionTypeExpense default: api.BadRequest(c, "Invalid transaction type") return } // Parse optional target currency var targetCurrency *models.Currency if req.TargetCurrency != nil && *req.TargetCurrency != "" { currency := models.Currency(*req.TargetCurrency) // Validate currency if !isValidCurrency(currency) { api.BadRequest(c, "Invalid target_currency") return } targetCurrency = ¤cy } // Parse optional conversion date var conversionDate *time.Time if req.ConversionDate != nil && *req.ConversionDate != "" { date, err := time.Parse("2006-01-02", *req.ConversionDate) if err != nil { api.BadRequest(c, "Invalid conversion_date format, expected YYYY-MM-DD") return } conversionDate = &date } // Get summary summary, err := h.reportService.GetCategorySummary(userId.(uint), startDate, endDate, transactionType, targetCurrency, conversionDate) if err != nil { api.InternalError(c, "Failed to get category summary: "+err.Error()) return } api.Success(c, summary) } // GetTrendData handles GET /api/v1/reports/trend func (h *ReportHandler) GetTrendData(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetTrendDataRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse period type var period service.PeriodType switch req.Period { case "day": period = service.PeriodTypeDay case "week": period = service.PeriodTypeWeek case "month": period = service.PeriodTypeMonth case "year": period = service.PeriodTypeYear default: api.BadRequest(c, "Invalid period type") return } // Parse optional currency var currency *models.Currency if req.Currency != nil && *req.Currency != "" { curr := models.Currency(*req.Currency) // Validate currency if !isValidCurrency(curr) { api.BadRequest(c, "Invalid currency") return } currency = &curr } // Get trend data trendData, err := h.reportService.GetTrendData(userId.(uint), startDate, endDate, period, currency) if err != nil { api.InternalError(c, "Failed to get trend data: "+err.Error()) return } api.Success(c, trendData) } // GetComparisonData handles GET /api/v1/reports/comparison func (h *ReportHandler) GetComparisonData(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetComparisonDataRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse optional currency var currency *models.Currency if req.Currency != nil && *req.Currency != "" { curr := models.Currency(*req.Currency) // Validate currency if !isValidCurrency(curr) { api.BadRequest(c, "Invalid currency") return } currency = &curr } // Get comparison data comparisonData, err := h.reportService.GetComparisonData(userId.(uint), startDate, endDate, currency) if err != nil { api.InternalError(c, "Failed to get comparison data: "+err.Error()) return } api.Success(c, comparisonData) } // GetAssetsSummary handles GET /api/v1/reports/assets func (h *ReportHandler) GetAssetsSummary(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetAssetsSummaryRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse optional target currency var targetCurrency *models.Currency if req.TargetCurrency != nil && *req.TargetCurrency != "" { currency := models.Currency(*req.TargetCurrency) // Validate currency if !isValidCurrency(currency) { api.BadRequest(c, "Invalid target_currency") return } targetCurrency = ¤cy } // Parse optional conversion date var conversionDate *time.Time if req.ConversionDate != nil && *req.ConversionDate != "" { date, err := time.Parse("2006-01-02", *req.ConversionDate) if err != nil { api.BadRequest(c, "Invalid conversion_date format, expected YYYY-MM-DD") return } conversionDate = &date } // Get assets summary summary, err := h.reportService.GetAssetsSummary(userId.(uint), targetCurrency, conversionDate) if err != nil { api.InternalError(c, "Failed to get assets summary: "+err.Error()) return } api.Success(c, summary) } // GetConsumptionHabitsRequest represents the request for consumption habits analysis type GetConsumptionHabitsRequest struct { StartDate string `form:"start_date" binding:"required"` EndDate string `form:"end_date" binding:"required"` Currency *string `form:"currency"` } // GetConsumptionHabits handles GET /api/v1/reports/consumption-habits func (h *ReportHandler) GetConsumptionHabits(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetConsumptionHabitsRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse optional currency var currency *models.Currency if req.Currency != nil && *req.Currency != "" { curr := models.Currency(*req.Currency) // Validate currency if !isValidCurrency(curr) { api.BadRequest(c, "Invalid currency") return } currency = &curr } // Get consumption habits habits, err := h.reportService.GetConsumptionHabits(userId.(uint), startDate, endDate, currency) if err != nil { api.InternalError(c, "Failed to get consumption habits: "+err.Error()) return } api.Success(c, habits) } // GetAssetLiabilityAnalysisRequest represents the request for asset liability analysis type GetAssetLiabilityAnalysisRequest struct { IncludeTrend bool `form:"include_trend"` TrendStartDate *string `form:"trend_start_date"` TrendEndDate *string `form:"trend_end_date"` } // GetAssetLiabilityAnalysis handles GET /api/v1/reports/asset-liability-analysis func (h *ReportHandler) GetAssetLiabilityAnalysis(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req GetAssetLiabilityAnalysisRequest if err := c.ShouldBindQuery(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse optional trend dates var trendStartDate, trendEndDate *time.Time if req.IncludeTrend { if req.TrendStartDate == nil || req.TrendEndDate == nil { api.BadRequest(c, "trend_start_date and trend_end_date are required when include_trend is true") return } startDate, err := time.Parse("2006-01-02", *req.TrendStartDate) if err != nil { api.BadRequest(c, "Invalid trend_start_date format, expected YYYY-MM-DD") return } trendStartDate = &startDate endDate, err := time.Parse("2006-01-02", *req.TrendEndDate) if err != nil { api.BadRequest(c, "Invalid trend_end_date format, expected YYYY-MM-DD") return } trendEndDate = &endDate // Validate date range if trendEndDate.Before(*trendStartDate) { api.BadRequest(c, "trend_end_date must be after trend_start_date") return } } // Get asset liability analysis analysis, err := h.reportService.GetAssetLiabilityAnalysis(userId.(uint), req.IncludeTrend, trendStartDate, trendEndDate) if err != nil { api.InternalError(c, "Failed to get asset liability analysis: "+err.Error()) return } api.Success(c, analysis) } // isValidCurrency checks if a currency is supported func isValidCurrency(currency models.Currency) bool { supportedCurrencies := models.SupportedCurrencies() for _, c := range supportedCurrencies { if c == currency { return true } } return false } // ExportReportRequest represents the request for exporting a report type ExportReportRequest struct { StartDate string `json:"start_date" binding:"required"` EndDate string `json:"end_date" binding:"required"` TargetCurrency *string `json:"target_currency"` Format string `json:"format" binding:"required,oneof=pdf excel"` } // ExportReport handles POST /api/v1/reports/export func (h *ReportHandler) ExportReport(c *gin.Context) { userId, exists := c.Get("user_id") if !exists { api.Unauthorized(c, "User not authenticated") return } var req ExportReportRequest if err := c.ShouldBindJSON(&req); err != nil { api.ValidationError(c, "Invalid request parameters: "+err.Error()) return } // Parse dates startDate, err := time.Parse("2006-01-02", req.StartDate) if err != nil { api.BadRequest(c, "Invalid start_date format, expected YYYY-MM-DD") return } endDate, err := time.Parse("2006-01-02", req.EndDate) if err != nil { api.BadRequest(c, "Invalid end_date format, expected YYYY-MM-DD") return } // Validate date range if endDate.Before(startDate) { api.BadRequest(c, "end_date must be after start_date") return } // Parse optional target currency var targetCurrency *models.Currency if req.TargetCurrency != nil && *req.TargetCurrency != "" { currency := models.Currency(*req.TargetCurrency) // Validate currency if !isValidCurrency(currency) { api.BadRequest(c, "Invalid target_currency") return } targetCurrency = ¤cy } // Handle different export formats switch req.Format { case "pdf": // Export as PDF exportReq := service.ExportReportRequest{ StartDate: startDate, EndDate: endDate, TargetCurrency: targetCurrency, IncludeCharts: true, } pdfData, err := h.pdfExportService.ExportReportToPDF(userId.(uint), exportReq) if err != nil { api.InternalError(c, "Failed to export report as PDF: "+err.Error()) return } // Set response headers for PDF download filename := fmt.Sprintf("report_%s_to_%s.pdf", startDate.Format("20060102"), endDate.Format("20060102")) c.Header("Content-Type", "application/pdf") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) c.Data(200, "application/pdf", pdfData) case "excel": // Export as Excel exportReq := service.ExportReportRequest{ StartDate: startDate, EndDate: endDate, TargetCurrency: targetCurrency, IncludeCharts: false, // Charts not supported in Excel yet } excelData, err := h.excelExportService.ExportReportToExcel(userId.(uint), exportReq) if err != nil { api.InternalError(c, "Failed to export report as Excel: "+err.Error()) return } // Set response headers for Excel download filename := fmt.Sprintf("report_%s_to_%s.xlsx", startDate.Format("20060102"), endDate.Format("20060102")) c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", excelData) default: api.BadRequest(c, "Unsupported export format") return } }