This commit is contained in:
2026-01-25 21:59:00 +08:00
parent 7fd537bef3
commit 4cad3f0250
118 changed files with 30473 additions and 0 deletions

View File

@@ -0,0 +1,619 @@
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 = &currency
}
// 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 = &currency
}
// 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 = &currency
}
// 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 = &currency
}
// 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
}
}