640 lines
21 KiB
Go
640 lines
21 KiB
Go
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
|
|
}
|