init
This commit is contained in:
639
internal/repository/report_repository.go
Normal file
639
internal/repository/report_repository.go
Normal file
@@ -0,0 +1,639 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"accounting-app/internal/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ReportRepository handles database operations for reports
|
||||
type ReportRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewReportRepository creates a new ReportRepository instance
|
||||
func NewReportRepository(db *gorm.DB) *ReportRepository {
|
||||
return &ReportRepository{db: db}
|
||||
}
|
||||
|
||||
// TransactionSummary represents aggregated transaction data
|
||||
type TransactionSummary struct {
|
||||
Currency models.Currency
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Balance float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
// CategorySummary represents aggregated data by category
|
||||
type CategorySummary struct {
|
||||
CategoryID uint
|
||||
CategoryName string
|
||||
Currency models.Currency
|
||||
TotalAmount float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
// GetTransactionSummaryByCurrency retrieves transaction summary grouped by currency
|
||||
func (r *ReportRepository) GetTransactionSummaryByCurrency(userID uint, startDate, endDate time.Time) ([]TransactionSummary, error) {
|
||||
// Query for income
|
||||
incomeQuery := r.db.Model(&models.Transaction{}).
|
||||
Select("currency, COALESCE(SUM(amount), 0) as total_income, COUNT(*) as count").
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND type = ?", userID, startDate, endDate, models.TransactionTypeIncome).
|
||||
Group("currency")
|
||||
|
||||
var incomeResults []struct {
|
||||
Currency string
|
||||
TotalIncome float64
|
||||
Count int64
|
||||
}
|
||||
if err := incomeQuery.Scan(&incomeResults).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get income summary: %w", err)
|
||||
}
|
||||
|
||||
// Query for expense
|
||||
expenseQuery := r.db.Model(&models.Transaction{}).
|
||||
Select("currency, COALESCE(SUM(amount), 0) as total_expense, COUNT(*) as count").
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND type = ?", userID, startDate, endDate, models.TransactionTypeExpense).
|
||||
Group("currency")
|
||||
|
||||
var expenseResults []struct {
|
||||
Currency string
|
||||
TotalExpense float64
|
||||
Count int64
|
||||
}
|
||||
if err := expenseQuery.Scan(&expenseResults).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get expense summary: %w", err)
|
||||
}
|
||||
|
||||
// Merge results by currency
|
||||
summaryMap := make(map[models.Currency]*TransactionSummary)
|
||||
|
||||
for _, income := range incomeResults {
|
||||
currency := models.Currency(income.Currency)
|
||||
if summaryMap[currency] == nil {
|
||||
summaryMap[currency] = &TransactionSummary{Currency: currency}
|
||||
}
|
||||
summaryMap[currency].TotalIncome = income.TotalIncome
|
||||
summaryMap[currency].Count += income.Count
|
||||
}
|
||||
|
||||
for _, expense := range expenseResults {
|
||||
currency := models.Currency(expense.Currency)
|
||||
if summaryMap[currency] == nil {
|
||||
summaryMap[currency] = &TransactionSummary{Currency: currency}
|
||||
}
|
||||
summaryMap[currency].TotalExpense = expense.TotalExpense
|
||||
summaryMap[currency].Count += expense.Count
|
||||
}
|
||||
|
||||
// Convert map to slice and calculate balance
|
||||
var summaries []TransactionSummary
|
||||
for _, summary := range summaryMap {
|
||||
summary.Balance = summary.TotalIncome - summary.TotalExpense
|
||||
summaries = append(summaries, *summary)
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// GetCategorySummaryByCurrency retrieves category summary grouped by currency
|
||||
func (r *ReportRepository) GetCategorySummaryByCurrency(userID uint, startDate, endDate time.Time, transactionType models.TransactionType) ([]CategorySummary, error) {
|
||||
var results []CategorySummary
|
||||
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("transactions.category_id, categories.name as category_name, transactions.currency, COALESCE(SUM(transactions.amount), 0) as total_amount, COUNT(*) as count").
|
||||
Joins("LEFT JOIN categories ON categories.id = transactions.category_id").
|
||||
Where("transactions.user_id = ? AND transactions.transaction_date >= ? AND transactions.transaction_date <= ? AND transactions.type = ?", userID, startDate, endDate, transactionType).
|
||||
Group("transactions.category_id, categories.name, transactions.currency").
|
||||
Order("total_amount DESC")
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get category summary: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTransactionSummaryAllCurrencies retrieves overall transaction summary (all currencies combined)
|
||||
func (r *ReportRepository) GetTransactionSummaryAllCurrencies(userID uint, startDate, endDate time.Time) (*TransactionSummary, error) {
|
||||
var result TransactionSummary
|
||||
|
||||
// Get total income
|
||||
var incomeResult struct {
|
||||
Total float64
|
||||
Count int64
|
||||
}
|
||||
if err := r.db.Model(&models.Transaction{}).
|
||||
Select("COALESCE(SUM(amount), 0) as total, COUNT(*) as count").
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND type = ?", userID, startDate, endDate, models.TransactionTypeIncome).
|
||||
Scan(&incomeResult).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get total income: %w", err)
|
||||
}
|
||||
result.TotalIncome = incomeResult.Total
|
||||
|
||||
// Get total expense
|
||||
var expenseResult struct {
|
||||
Total float64
|
||||
Count int64
|
||||
}
|
||||
if err := r.db.Model(&models.Transaction{}).
|
||||
Select("COALESCE(SUM(amount), 0) as total, COUNT(*) as count").
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND type = ?", userID, startDate, endDate, models.TransactionTypeExpense).
|
||||
Scan(&expenseResult).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get total expense: %w", err)
|
||||
}
|
||||
result.TotalExpense = expenseResult.Total
|
||||
|
||||
result.Count = incomeResult.Count + expenseResult.Count
|
||||
result.Balance = result.TotalIncome - result.TotalExpense
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GetCategorySummaryAllCurrencies retrieves category summary (all currencies combined)
|
||||
func (r *ReportRepository) GetCategorySummaryAllCurrencies(userID uint, startDate, endDate time.Time, transactionType models.TransactionType) ([]CategorySummary, error) {
|
||||
var results []CategorySummary
|
||||
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("transactions.category_id, categories.name as category_name, COALESCE(SUM(transactions.amount), 0) as total_amount, COUNT(*) as count").
|
||||
Joins("LEFT JOIN categories ON categories.id = transactions.category_id").
|
||||
Where("transactions.user_id = ? AND transactions.transaction_date >= ? AND transactions.transaction_date <= ? AND transactions.type = ?", userID, startDate, endDate, transactionType).
|
||||
Group("transactions.category_id, categories.name").
|
||||
Order("total_amount DESC")
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get category summary: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetTransactionsByCurrency retrieves all transactions for a specific currency in a date range
|
||||
func (r *ReportRepository) GetTransactionsByCurrency(userID uint, startDate, endDate time.Time, currency models.Currency) ([]models.Transaction, error) {
|
||||
var transactions []models.Transaction
|
||||
if err := r.db.Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND currency = ?", userID, startDate, endDate, currency).
|
||||
Order("transaction_date DESC").
|
||||
Preload("Category").
|
||||
Preload("Account").
|
||||
Preload("Tags").
|
||||
Find(&transactions).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get transactions by currency: %w", err)
|
||||
}
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
// TrendDataPoint represents a single point in trend data
|
||||
type TrendDataPoint struct {
|
||||
Date time.Time
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Balance float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
// GetTrendDataByDay retrieves daily trend data
|
||||
func (r *ReportRepository) GetTrendDataByDay(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]TrendDataPoint, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("DATE(transaction_date) as date, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_income, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_expense, "+
|
||||
"COUNT(*) as count", models.TransactionTypeIncome, models.TransactionTypeExpense).
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ?", userID, startDate, endDate)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("DATE(transaction_date)").Order("date ASC")
|
||||
|
||||
var results []struct {
|
||||
Date string
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get daily trend data: %w", err)
|
||||
}
|
||||
|
||||
// Convert to TrendDataPoint
|
||||
trendData := make([]TrendDataPoint, 0, len(results))
|
||||
for _, result := range results {
|
||||
date, err := parseFlexibleDate(result.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse date: %w", err)
|
||||
}
|
||||
trendData = append(trendData, TrendDataPoint{
|
||||
Date: date,
|
||||
TotalIncome: result.TotalIncome,
|
||||
TotalExpense: result.TotalExpense,
|
||||
Balance: result.TotalIncome - result.TotalExpense,
|
||||
Count: result.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return trendData, nil
|
||||
}
|
||||
|
||||
// parseFlexibleDate parses date strings in various formats
|
||||
func parseFlexibleDate(dateStr string) (time.Time, error) {
|
||||
// Try different date formats
|
||||
formats := []string{
|
||||
"2006-01-02",
|
||||
"2006-01-02T15:04:05Z07:00",
|
||||
"2006-01-02T15:04:05+08:00",
|
||||
"2006-01-02T15:04:05",
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339,
|
||||
time.RFC3339Nano,
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
if t, err := time.Parse(format, dateStr); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
// If all formats fail, try to extract just the date part
|
||||
if len(dateStr) >= 10 {
|
||||
if t, err := time.Parse("2006-01-02", dateStr[:10]); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}, fmt.Errorf("unable to parse date: %s", dateStr)
|
||||
}
|
||||
|
||||
// GetTrendDataByWeek retrieves weekly trend data
|
||||
func (r *ReportRepository) GetTrendDataByWeek(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]TrendDataPoint, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("YEARWEEK(transaction_date, 1) as week, "+
|
||||
"MIN(DATE(transaction_date)) as date, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_income, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_expense, "+
|
||||
"COUNT(*) as count", models.TransactionTypeIncome, models.TransactionTypeExpense).
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ?", userID, startDate, endDate)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("YEARWEEK(transaction_date, 1)").Order("week ASC")
|
||||
|
||||
var results []struct {
|
||||
Week string
|
||||
Date string
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get weekly trend data: %w", err)
|
||||
}
|
||||
|
||||
// Convert to TrendDataPoint
|
||||
trendData := make([]TrendDataPoint, 0, len(results))
|
||||
for _, result := range results {
|
||||
date, err := parseFlexibleDate(result.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse date: %w", err)
|
||||
}
|
||||
trendData = append(trendData, TrendDataPoint{
|
||||
Date: date,
|
||||
TotalIncome: result.TotalIncome,
|
||||
TotalExpense: result.TotalExpense,
|
||||
Balance: result.TotalIncome - result.TotalExpense,
|
||||
Count: result.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return trendData, nil
|
||||
}
|
||||
|
||||
// GetTrendDataByMonth retrieves monthly trend data
|
||||
func (r *ReportRepository) GetTrendDataByMonth(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]TrendDataPoint, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("DATE_FORMAT(transaction_date, '%Y-%m') as month, "+
|
||||
"DATE_FORMAT(transaction_date, '%Y-%m-01') as date, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_income, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_expense, "+
|
||||
"COUNT(*) as count", models.TransactionTypeIncome, models.TransactionTypeExpense).
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ?", userID, startDate, endDate)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("DATE_FORMAT(transaction_date, '%Y-%m')").Order("month ASC")
|
||||
|
||||
var results []struct {
|
||||
Month string
|
||||
Date string
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get monthly trend data: %w", err)
|
||||
}
|
||||
|
||||
// Convert to TrendDataPoint
|
||||
trendData := make([]TrendDataPoint, 0, len(results))
|
||||
for _, result := range results {
|
||||
date, err := parseFlexibleDate(result.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse date: %w", err)
|
||||
}
|
||||
trendData = append(trendData, TrendDataPoint{
|
||||
Date: date,
|
||||
TotalIncome: result.TotalIncome,
|
||||
TotalExpense: result.TotalExpense,
|
||||
Balance: result.TotalIncome - result.TotalExpense,
|
||||
Count: result.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return trendData, nil
|
||||
}
|
||||
|
||||
// GetTrendDataByYear retrieves yearly trend data
|
||||
func (r *ReportRepository) GetTrendDataByYear(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]TrendDataPoint, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("YEAR(transaction_date) as year, "+
|
||||
"CONCAT(YEAR(transaction_date), '-01-01') as date, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_income, "+
|
||||
"COALESCE(SUM(CASE WHEN type = ? THEN amount ELSE 0 END), 0) as total_expense, "+
|
||||
"COUNT(*) as count", models.TransactionTypeIncome, models.TransactionTypeExpense).
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ?", userID, startDate, endDate)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("YEAR(transaction_date)").Order("year ASC")
|
||||
|
||||
var results []struct {
|
||||
Year string
|
||||
Date string
|
||||
TotalIncome float64
|
||||
TotalExpense float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get yearly trend data: %w", err)
|
||||
}
|
||||
|
||||
// Convert to TrendDataPoint
|
||||
trendData := make([]TrendDataPoint, 0, len(results))
|
||||
for _, result := range results {
|
||||
date, err := parseFlexibleDate(result.Date)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse date: %w", err)
|
||||
}
|
||||
trendData = append(trendData, TrendDataPoint{
|
||||
Date: date,
|
||||
TotalIncome: result.TotalIncome,
|
||||
TotalExpense: result.TotalExpense,
|
||||
Balance: result.TotalIncome - result.TotalExpense,
|
||||
Count: result.Count,
|
||||
})
|
||||
}
|
||||
|
||||
return trendData, nil
|
||||
}
|
||||
|
||||
// AssetsSummary represents assets and liabilities summary
|
||||
type AssetsSummary struct {
|
||||
Currency models.Currency
|
||||
TotalAssets float64
|
||||
TotalLiabilities float64
|
||||
NetAssets float64
|
||||
AccountCount int64
|
||||
}
|
||||
|
||||
// GetAssetsSummaryByCurrency retrieves assets and liabilities summary grouped by currency
|
||||
func (r *ReportRepository) GetAssetsSummaryByCurrency(userID uint) ([]AssetsSummary, error) {
|
||||
var results []struct {
|
||||
Currency string
|
||||
TotalAssets float64
|
||||
TotalLiabilities float64
|
||||
AccountCount int64
|
||||
}
|
||||
|
||||
// Query to get assets (positive balances) and liabilities (negative balances) by currency
|
||||
query := r.db.Model(&models.Account{}).
|
||||
Select("currency, "+
|
||||
"COALESCE(SUM(CASE WHEN balance >= 0 THEN balance ELSE 0 END), 0) as total_assets, "+
|
||||
"COALESCE(SUM(CASE WHEN balance < 0 THEN -balance ELSE 0 END), 0) as total_liabilities, "+
|
||||
"COUNT(*) as account_count").
|
||||
Where("user_id = ?", userID).
|
||||
Group("currency")
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get assets summary: %w", err)
|
||||
}
|
||||
|
||||
// Convert to AssetsSummary
|
||||
summaries := make([]AssetsSummary, 0, len(results))
|
||||
for _, result := range results {
|
||||
summaries = append(summaries, AssetsSummary{
|
||||
Currency: models.Currency(result.Currency),
|
||||
TotalAssets: result.TotalAssets,
|
||||
TotalLiabilities: result.TotalLiabilities,
|
||||
NetAssets: result.TotalAssets - result.TotalLiabilities,
|
||||
AccountCount: result.AccountCount,
|
||||
})
|
||||
}
|
||||
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
// GetAccountsByBalanceType retrieves accounts grouped by balance type (assets or liabilities)
|
||||
func (r *ReportRepository) GetAccountsByBalanceType(userID uint, currency *models.Currency) (assets []models.Account, liabilities []models.Account, err error) {
|
||||
query := r.db.Model(&models.Account{}).Where("user_id = ?", userID)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
var accounts []models.Account
|
||||
if err := query.Find(&accounts).Error; err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get accounts: %w", err)
|
||||
}
|
||||
|
||||
// Separate into assets and liabilities
|
||||
for _, account := range accounts {
|
||||
if account.Balance >= 0 {
|
||||
assets = append(assets, account)
|
||||
} else {
|
||||
liabilities = append(liabilities, account)
|
||||
}
|
||||
}
|
||||
|
||||
return assets, liabilities, nil
|
||||
}
|
||||
|
||||
// GetSpendingByHour retrieves spending grouped by hour of day
|
||||
func (r *ReportRepository) GetSpendingByHour(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]HourSummary, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("HOUR(created_at) as hour, "+
|
||||
"COALESCE(SUM(amount), 0) as total_amount, "+
|
||||
"COUNT(*) as count").
|
||||
Where("user_id = ? AND transaction_date >= ? AND transaction_date <= ? AND type = ?", userID, startDate, endDate, models.TransactionTypeExpense)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("HOUR(created_at)").Order("hour ASC")
|
||||
|
||||
var results []struct {
|
||||
Hour int
|
||||
TotalAmount float64
|
||||
Count int64
|
||||
}
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get spending by hour: %w", err)
|
||||
}
|
||||
|
||||
// Convert to HourSummary
|
||||
hourSummaries := make([]HourSummary, 0, len(results))
|
||||
for _, result := range results {
|
||||
avgAmount := 0.0
|
||||
if result.Count > 0 {
|
||||
avgAmount = result.TotalAmount / float64(result.Count)
|
||||
}
|
||||
hourSummaries = append(hourSummaries, HourSummary{
|
||||
Hour: result.Hour,
|
||||
TotalAmount: result.TotalAmount,
|
||||
Count: result.Count,
|
||||
AvgAmount: avgAmount,
|
||||
})
|
||||
}
|
||||
|
||||
return hourSummaries, nil
|
||||
}
|
||||
|
||||
// HourSummary represents spending by hour of day
|
||||
type HourSummary struct {
|
||||
Hour int
|
||||
TotalAmount float64
|
||||
Count int64
|
||||
AvgAmount float64
|
||||
}
|
||||
|
||||
// GetCommonScenarios retrieves common spending scenarios (categories)
|
||||
func (r *ReportRepository) GetCommonScenarios(userID uint, startDate, endDate time.Time, currency *models.Currency) ([]ScenarioSummary, error) {
|
||||
query := r.db.Model(&models.Transaction{}).
|
||||
Select("transactions.category_id, categories.name as category_name, "+
|
||||
"COALESCE(SUM(transactions.amount), 0) as total_amount, "+
|
||||
"COUNT(*) as count").
|
||||
Joins("LEFT JOIN categories ON categories.id = transactions.category_id").
|
||||
Where("transactions.user_id = ? AND transactions.transaction_date >= ? AND transactions.transaction_date <= ? AND transactions.type = ?", userID, startDate, endDate, models.TransactionTypeExpense)
|
||||
|
||||
if currency != nil {
|
||||
query = query.Where("transactions.currency = ?", *currency)
|
||||
}
|
||||
|
||||
query = query.Group("transactions.category_id, categories.name").
|
||||
Order("count DESC").
|
||||
Limit(10) // Top 10 most frequent scenarios
|
||||
|
||||
var results []ScenarioSummary
|
||||
|
||||
if err := query.Scan(&results).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get common scenarios: %w", err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// ScenarioSummary represents spending by category (scenario)
|
||||
type ScenarioSummary struct {
|
||||
CategoryID uint
|
||||
CategoryName string
|
||||
TotalAmount float64
|
||||
Count int64
|
||||
Frequency float64
|
||||
}
|
||||
|
||||
// GetAllAccounts retrieves all accounts
|
||||
func (r *ReportRepository) GetAllAccounts(userID uint) ([]models.Account, error) {
|
||||
var accounts []models.Account
|
||||
if err := r.db.Where("user_id = ?", userID).Find(&accounts).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get all accounts: %w", err)
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetAssetTrend retrieves asset trend over time
|
||||
func (r *ReportRepository) GetAssetTrend(userID uint, startDate, endDate time.Time) ([]AssetTrendPoint, error) {
|
||||
// This is a simplified implementation that calculates daily snapshots
|
||||
// In a real system, you might want to store daily snapshots for better performance
|
||||
|
||||
var trendPoints []AssetTrendPoint
|
||||
|
||||
// Generate daily points
|
||||
currentDate := startDate
|
||||
for currentDate.Before(endDate) || currentDate.Equal(endDate) {
|
||||
// Calculate balance at end of this day
|
||||
// This requires summing all transactions up to this date
|
||||
var accounts []models.Account
|
||||
if err := r.db.Where("user_id = ?", userID).Find(&accounts).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get accounts: %w", err)
|
||||
}
|
||||
|
||||
var totalAssets, totalLiabilities float64
|
||||
for _, account := range accounts {
|
||||
// Get initial balance (would need to be stored separately in real system)
|
||||
// For now, we'll use current balance and adjust by transactions
|
||||
balance := account.Balance
|
||||
|
||||
// Adjust by transactions after currentDate
|
||||
var futureTransactions []models.Transaction
|
||||
r.db.Where("user_id = ? AND account_id = ? AND transaction_date > ?", userID, account.ID, currentDate).Find(&futureTransactions)
|
||||
|
||||
for _, txn := range futureTransactions {
|
||||
if txn.Type == models.TransactionTypeIncome {
|
||||
balance -= txn.Amount
|
||||
} else if txn.Type == models.TransactionTypeExpense {
|
||||
balance += txn.Amount
|
||||
}
|
||||
}
|
||||
|
||||
if balance >= 0 {
|
||||
totalAssets += balance
|
||||
} else {
|
||||
totalLiabilities += -balance
|
||||
}
|
||||
}
|
||||
|
||||
trendPoints = append(trendPoints, AssetTrendPoint{
|
||||
Date: currentDate,
|
||||
TotalAssets: totalAssets,
|
||||
TotalLiabilities: totalLiabilities,
|
||||
NetAssets: totalAssets - totalLiabilities,
|
||||
})
|
||||
|
||||
currentDate = currentDate.AddDate(0, 0, 1)
|
||||
}
|
||||
|
||||
return trendPoints, nil
|
||||
}
|
||||
|
||||
// AssetTrendPoint represents a point in asset trend
|
||||
type AssetTrendPoint struct {
|
||||
Date time.Time
|
||||
TotalAssets float64
|
||||
TotalLiabilities float64
|
||||
NetAssets float64
|
||||
}
|
||||
Reference in New Issue
Block a user