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,723 @@
package service
import (
"fmt"
"time"
"accounting-app/internal/models"
"accounting-app/internal/repository"
)
// ReportService handles business logic for reports
type ReportService struct {
reportRepo *repository.ReportRepository
exchangeRateRepo *repository.ExchangeRateRepository
}
// NewReportService creates a new ReportService instance
func NewReportService(reportRepo *repository.ReportRepository, exchangeRateRepo *repository.ExchangeRateRepository) *ReportService {
return &ReportService{
reportRepo: reportRepo,
exchangeRateRepo: exchangeRateRepo,
}
}
// MultiCurrencySummary represents a summary that can be displayed in multiple ways
type MultiCurrencySummary struct {
// Separated by currency
ByCurrency []CurrencySummary `json:"by_currency"`
// Converted to a single currency (if requested)
Unified *UnifiedSummary `json:"unified,omitempty"`
}
// CurrencySummary represents summary for a single currency
type CurrencySummary struct {
Currency models.Currency `json:"currency"`
TotalIncome float64 `json:"total_income"`
TotalExpense float64 `json:"total_expense"`
Balance float64 `json:"balance"`
Count int64 `json:"count"`
}
// UnifiedSummary represents summary converted to a single currency
type UnifiedSummary struct {
TargetCurrency models.Currency `json:"target_currency"`
TotalIncome float64 `json:"total_income"`
TotalExpense float64 `json:"total_expense"`
Balance float64 `json:"balance"`
ConversionDate time.Time `json:"conversion_date"`
}
// CategorySummary represents category-level summary
type CategorySummary struct {
CategoryID uint `json:"category_id"`
CategoryName string `json:"category_name"`
Currency models.Currency `json:"currency,omitempty"`
TotalAmount float64 `json:"total_amount"`
Count int64 `json:"count"`
Percentage float64 `json:"percentage,omitempty"`
}
// MultiCurrencyCategorySummary represents category summary with multi-currency support
type MultiCurrencyCategorySummary struct {
// Separated by currency
ByCurrency []CategorySummary `json:"by_currency"`
// Converted to a single currency (if requested)
Unified []CategorySummary `json:"unified,omitempty"`
}
// GetTransactionSummary retrieves transaction summary with multi-currency support
func (s *ReportService) GetTransactionSummary(userID uint, startDate, endDate time.Time, targetCurrency *models.Currency, conversionDate *time.Time) (*MultiCurrencySummary, error) {
// Get summary by currency
summaries, err := s.reportRepo.GetTransactionSummaryByCurrency(userID, startDate, endDate)
if err != nil {
return nil, fmt.Errorf("failed to get transaction summary: %w", err)
}
// Convert to response format
result := &MultiCurrencySummary{
ByCurrency: make([]CurrencySummary, 0, len(summaries)),
}
for _, summary := range summaries {
result.ByCurrency = append(result.ByCurrency, CurrencySummary{
Currency: summary.Currency,
TotalIncome: summary.TotalIncome,
TotalExpense: summary.TotalExpense,
Balance: summary.Balance,
Count: summary.Count,
})
}
// If target currency is specified, convert all to that currency
if targetCurrency != nil {
unified, err := s.convertToUnifiedSummary(summaries, *targetCurrency, conversionDate)
if err != nil {
return nil, fmt.Errorf("failed to convert to unified summary: %w", err)
}
result.Unified = unified
}
return result, nil
}
// GetCategorySummary retrieves category summary with multi-currency support
func (s *ReportService) GetCategorySummary(userID uint, startDate, endDate time.Time, transactionType models.TransactionType, targetCurrency *models.Currency, conversionDate *time.Time) (*MultiCurrencyCategorySummary, error) {
// Get summary by currency
summaries, err := s.reportRepo.GetCategorySummaryByCurrency(userID, startDate, endDate, transactionType)
if err != nil {
return nil, fmt.Errorf("failed to get category summary: %w", err)
}
// Convert to response format
result := &MultiCurrencyCategorySummary{
ByCurrency: make([]CategorySummary, 0, len(summaries)),
}
// Calculate total for percentage
var totalByCurrency = make(map[models.Currency]float64)
for _, summary := range summaries {
totalByCurrency[summary.Currency] += summary.TotalAmount
}
for _, summary := range summaries {
percentage := 0.0
if totalByCurrency[summary.Currency] > 0 {
percentage = (summary.TotalAmount / totalByCurrency[summary.Currency]) * 100
}
result.ByCurrency = append(result.ByCurrency, CategorySummary{
CategoryID: summary.CategoryID,
CategoryName: summary.CategoryName,
Currency: summary.Currency,
TotalAmount: summary.TotalAmount,
Count: summary.Count,
Percentage: percentage,
})
}
// If target currency is specified, convert all to that currency
if targetCurrency != nil {
unified, err := s.convertToUnifiedCategorySummary(summaries, *targetCurrency, conversionDate)
if err != nil {
return nil, fmt.Errorf("failed to convert to unified category summary: %w", err)
}
// Calculate percentages for unified view
var total float64
for _, cat := range unified {
total += cat.TotalAmount
}
for i := range unified {
if total > 0 {
unified[i].Percentage = (unified[i].TotalAmount / total) * 100
}
}
result.Unified = unified
}
return result, nil
}
// convertToUnifiedSummary converts multiple currency summaries to a single currency
func (s *ReportService) convertToUnifiedSummary(summaries []repository.TransactionSummary, targetCurrency models.Currency, conversionDate *time.Time) (*UnifiedSummary, error) {
// Use current date if not specified
date := time.Now()
if conversionDate != nil {
date = *conversionDate
}
unified := &UnifiedSummary{
TargetCurrency: targetCurrency,
ConversionDate: date,
}
for _, summary := range summaries {
// If already in target currency, no conversion needed
if summary.Currency == targetCurrency {
unified.TotalIncome += summary.TotalIncome
unified.TotalExpense += summary.TotalExpense
continue
}
// Get exchange rate
rate, err := s.exchangeRateRepo.GetByCurrencyPairAndDate(summary.Currency, targetCurrency, date)
if err != nil {
// Try inverse rate
inverseRate, inverseErr := s.exchangeRateRepo.GetByCurrencyPairAndDate(targetCurrency, summary.Currency, date)
if inverseErr != nil {
return nil, fmt.Errorf("exchange rate not found for %s to %s on %s: %w", summary.Currency, targetCurrency, date.Format("2006-01-02"), err)
}
rate = &models.ExchangeRate{
FromCurrency: summary.Currency,
ToCurrency: targetCurrency,
Rate: 1.0 / inverseRate.Rate,
}
}
// Convert amounts
unified.TotalIncome += summary.TotalIncome * rate.Rate
unified.TotalExpense += summary.TotalExpense * rate.Rate
}
unified.Balance = unified.TotalIncome - unified.TotalExpense
return unified, nil
}
// convertToUnifiedCategorySummary converts multiple currency category summaries to a single currency
func (s *ReportService) convertToUnifiedCategorySummary(summaries []repository.CategorySummary, targetCurrency models.Currency, conversionDate *time.Time) ([]CategorySummary, error) {
// Use current date if not specified
date := time.Now()
if conversionDate != nil {
date = *conversionDate
}
// Group by category
categoryMap := make(map[uint]*CategorySummary)
for _, summary := range summaries {
// Initialize category if not exists
if categoryMap[summary.CategoryID] == nil {
categoryMap[summary.CategoryID] = &CategorySummary{
CategoryID: summary.CategoryID,
CategoryName: summary.CategoryName,
TotalAmount: 0,
Count: 0,
}
}
// If already in target currency, no conversion needed
if summary.Currency == targetCurrency {
categoryMap[summary.CategoryID].TotalAmount += summary.TotalAmount
categoryMap[summary.CategoryID].Count += summary.Count
continue
}
// Get exchange rate
rate, err := s.exchangeRateRepo.GetByCurrencyPairAndDate(summary.Currency, targetCurrency, date)
if err != nil {
// Try inverse rate
inverseRate, inverseErr := s.exchangeRateRepo.GetByCurrencyPairAndDate(targetCurrency, summary.Currency, date)
if inverseErr != nil {
return nil, fmt.Errorf("exchange rate not found for %s to %s on %s: %w", summary.Currency, targetCurrency, date.Format("2006-01-02"), err)
}
rate = &models.ExchangeRate{
FromCurrency: summary.Currency,
ToCurrency: targetCurrency,
Rate: 1.0 / inverseRate.Rate,
}
}
// Convert amount
categoryMap[summary.CategoryID].TotalAmount += summary.TotalAmount * rate.Rate
categoryMap[summary.CategoryID].Count += summary.Count
}
// Convert map to slice
result := make([]CategorySummary, 0, len(categoryMap))
for _, cat := range categoryMap {
result = append(result, *cat)
}
return result, nil
}
// PeriodType represents the time period for trend analysis
type PeriodType string
const (
PeriodTypeDay PeriodType = "day"
PeriodTypeWeek PeriodType = "week"
PeriodTypeMonth PeriodType = "month"
PeriodTypeYear PeriodType = "year"
)
// TrendData represents trend analysis data
type TrendData struct {
Period PeriodType `json:"period"`
Currency *models.Currency `json:"currency,omitempty"`
DataPoints []repository.TrendDataPoint `json:"data_points"`
}
// GetTrendData retrieves trend data for the specified period
func (s *ReportService) GetTrendData(userID uint, startDate, endDate time.Time, period PeriodType, currency *models.Currency) (*TrendData, error) {
var dataPoints []repository.TrendDataPoint
var err error
switch period {
case PeriodTypeDay:
dataPoints, err = s.reportRepo.GetTrendDataByDay(userID, startDate, endDate, currency)
case PeriodTypeWeek:
dataPoints, err = s.reportRepo.GetTrendDataByWeek(userID, startDate, endDate, currency)
case PeriodTypeMonth:
dataPoints, err = s.reportRepo.GetTrendDataByMonth(userID, startDate, endDate, currency)
case PeriodTypeYear:
dataPoints, err = s.reportRepo.GetTrendDataByYear(userID, startDate, endDate, currency)
default:
return nil, fmt.Errorf("invalid period type: %s", period)
}
if err != nil {
return nil, fmt.Errorf("failed to get trend data: %w", err)
}
return &TrendData{
Period: period,
Currency: currency,
DataPoints: dataPoints,
}, nil
}
// ComparisonData represents comparison analysis data (YoY and MoM)
type ComparisonData struct {
Current PeriodSummary `json:"current"`
Previous PeriodSummary `json:"previous"`
YearAgo PeriodSummary `json:"year_ago,omitempty"`
Changes Changes `json:"changes"`
}
// PeriodSummary represents summary for a specific period
type PeriodSummary struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
TotalIncome float64 `json:"total_income"`
TotalExpense float64 `json:"total_expense"`
Balance float64 `json:"balance"`
}
// Changes represents the changes between periods
type Changes struct {
IncomeChange float64 `json:"income_change"`
IncomeChangePercent float64 `json:"income_change_percent"`
ExpenseChange float64 `json:"expense_change"`
ExpenseChangePercent float64 `json:"expense_change_percent"`
BalanceChange float64 `json:"balance_change"`
BalanceChangePercent float64 `json:"balance_change_percent"`
YoYIncomeChange float64 `json:"yoy_income_change,omitempty"`
YoYIncomeChangePercent float64 `json:"yoy_income_change_percent,omitempty"`
YoYExpenseChange float64 `json:"yoy_expense_change,omitempty"`
YoYExpenseChangePercent float64 `json:"yoy_expense_change_percent,omitempty"`
}
// GetComparisonData retrieves comparison data (MoM and YoY)
func (s *ReportService) GetComparisonData(userID uint, currentStart, currentEnd time.Time, currency *models.Currency) (*ComparisonData, error) {
// Calculate previous period (same duration)
duration := currentEnd.Sub(currentStart)
previousEnd := currentStart.AddDate(0, 0, -1)
previousStart := previousEnd.Add(-duration)
// Calculate year ago period
yearAgoStart := currentStart.AddDate(-1, 0, 0)
yearAgoEnd := currentEnd.AddDate(-1, 0, 0)
// Get current period summary
currentSummary, err := s.getPeriodSummary(userID, currentStart, currentEnd, currency)
if err != nil {
return nil, fmt.Errorf("failed to get current period summary: %w", err)
}
// Get previous period summary
previousSummary, err := s.getPeriodSummary(userID, previousStart, previousEnd, currency)
if err != nil {
return nil, fmt.Errorf("failed to get previous period summary: %w", err)
}
// Get year ago period summary
yearAgoSummary, err := s.getPeriodSummary(userID, yearAgoStart, yearAgoEnd, currency)
if err != nil {
return nil, fmt.Errorf("failed to get year ago period summary: %w", err)
}
// Calculate changes
changes := Changes{
IncomeChange: currentSummary.TotalIncome - previousSummary.TotalIncome,
ExpenseChange: currentSummary.TotalExpense - previousSummary.TotalExpense,
BalanceChange: currentSummary.Balance - previousSummary.Balance,
}
// Calculate percentage changes (MoM)
if previousSummary.TotalIncome > 0 {
changes.IncomeChangePercent = (changes.IncomeChange / previousSummary.TotalIncome) * 100
}
if previousSummary.TotalExpense > 0 {
changes.ExpenseChangePercent = (changes.ExpenseChange / previousSummary.TotalExpense) * 100
}
if previousSummary.Balance != 0 {
changes.BalanceChangePercent = (changes.BalanceChange / previousSummary.Balance) * 100
}
// Calculate YoY changes
changes.YoYIncomeChange = currentSummary.TotalIncome - yearAgoSummary.TotalIncome
changes.YoYExpenseChange = currentSummary.TotalExpense - yearAgoSummary.TotalExpense
if yearAgoSummary.TotalIncome > 0 {
changes.YoYIncomeChangePercent = (changes.YoYIncomeChange / yearAgoSummary.TotalIncome) * 100
}
if yearAgoSummary.TotalExpense > 0 {
changes.YoYExpenseChangePercent = (changes.YoYExpenseChange / yearAgoSummary.TotalExpense) * 100
}
return &ComparisonData{
Current: *currentSummary,
Previous: *previousSummary,
YearAgo: *yearAgoSummary,
Changes: changes,
}, nil
}
// getPeriodSummary is a helper function to get summary for a specific period
func (s *ReportService) getPeriodSummary(userID uint, startDate, endDate time.Time, currency *models.Currency) (*PeriodSummary, error) {
var totalIncome, totalExpense float64
// If currency is specified, filter by currency
if currency != nil {
// Get transactions for the period and currency
dataPoints, err := s.reportRepo.GetTrendDataByDay(userID, startDate, endDate, currency)
if err != nil {
return nil, err
}
// Sum up the data points
for _, dp := range dataPoints {
totalIncome += dp.TotalIncome
totalExpense += dp.TotalExpense
}
} else {
// Get all currencies
summaries, err := s.reportRepo.GetTransactionSummaryByCurrency(userID, startDate, endDate)
if err != nil {
return nil, err
}
// Sum up all currencies
for _, summary := range summaries {
totalIncome += summary.TotalIncome
totalExpense += summary.TotalExpense
}
}
return &PeriodSummary{
StartDate: startDate,
EndDate: endDate,
TotalIncome: totalIncome,
TotalExpense: totalExpense,
Balance: totalIncome - totalExpense,
}, nil
}
// AssetsSummaryResponse represents assets and liabilities overview
type AssetsSummaryResponse struct {
ByCurrency []AssetsCurrencySummary `json:"by_currency"`
Unified *UnifiedAssetsSummary `json:"unified,omitempty"`
}
// AssetsCurrencySummary represents assets summary for a single currency
type AssetsCurrencySummary struct {
Currency models.Currency `json:"currency"`
TotalAssets float64 `json:"total_assets"`
TotalLiabilities float64 `json:"total_liabilities"`
NetAssets float64 `json:"net_assets"`
AccountCount int64 `json:"account_count"`
}
// UnifiedAssetsSummary represents assets summary converted to a single currency
type UnifiedAssetsSummary struct {
TargetCurrency models.Currency `json:"target_currency"`
TotalAssets float64 `json:"total_assets"`
TotalLiabilities float64 `json:"total_liabilities"`
NetAssets float64 `json:"net_assets"`
ConversionDate time.Time `json:"conversion_date"`
}
// GetAssetsSummary retrieves assets and liabilities overview
func (s *ReportService) GetAssetsSummary(userID uint, targetCurrency *models.Currency, conversionDate *time.Time) (*AssetsSummaryResponse, error) {
// Get summary by currency
summaries, err := s.reportRepo.GetAssetsSummaryByCurrency(userID)
if err != nil {
return nil, fmt.Errorf("failed to get assets summary: %w", err)
}
// Convert to response format
result := &AssetsSummaryResponse{
ByCurrency: make([]AssetsCurrencySummary, 0, len(summaries)),
}
for _, summary := range summaries {
result.ByCurrency = append(result.ByCurrency, AssetsCurrencySummary{
Currency: summary.Currency,
TotalAssets: summary.TotalAssets,
TotalLiabilities: summary.TotalLiabilities,
NetAssets: summary.NetAssets,
AccountCount: summary.AccountCount,
})
}
// If target currency is specified, convert all to that currency
if targetCurrency != nil {
unified, err := s.convertToUnifiedAssetsSummary(summaries, *targetCurrency, conversionDate)
if err != nil {
return nil, fmt.Errorf("failed to convert to unified assets summary: %w", err)
}
result.Unified = unified
}
return result, nil
}
// convertToUnifiedAssetsSummary converts multiple currency assets summaries to a single currency
func (s *ReportService) convertToUnifiedAssetsSummary(summaries []repository.AssetsSummary, targetCurrency models.Currency, conversionDate *time.Time) (*UnifiedAssetsSummary, error) {
// Use current date if not specified
date := time.Now()
if conversionDate != nil {
date = *conversionDate
}
unified := &UnifiedAssetsSummary{
TargetCurrency: targetCurrency,
ConversionDate: date,
}
for _, summary := range summaries {
// If already in target currency, no conversion needed
if summary.Currency == targetCurrency {
unified.TotalAssets += summary.TotalAssets
unified.TotalLiabilities += summary.TotalLiabilities
continue
}
// Get exchange rate
rate, err := s.exchangeRateRepo.GetByCurrencyPairAndDate(summary.Currency, targetCurrency, date)
if err != nil {
// Try inverse rate
inverseRate, inverseErr := s.exchangeRateRepo.GetByCurrencyPairAndDate(targetCurrency, summary.Currency, date)
if inverseErr != nil {
return nil, fmt.Errorf("exchange rate not found for %s to %s on %s: %w", summary.Currency, targetCurrency, date.Format("2006-01-02"), err)
}
rate = &models.ExchangeRate{
FromCurrency: summary.Currency,
ToCurrency: targetCurrency,
Rate: 1.0 / inverseRate.Rate,
}
}
// Convert amounts
unified.TotalAssets += summary.TotalAssets * rate.Rate
unified.TotalLiabilities += summary.TotalLiabilities * rate.Rate
}
unified.NetAssets = unified.TotalAssets - unified.TotalLiabilities
return unified, nil
}
// ConsumptionHabit represents consumption habit analysis
type ConsumptionHabit struct {
PeakHours []HourSummary `json:"peak_hours"`
CommonScenarios []ScenarioSummary `json:"common_scenarios"`
}
// HourSummary represents spending by hour of day
type HourSummary struct {
Hour int `json:"hour"`
TotalAmount float64 `json:"total_amount"`
Count int64 `json:"count"`
AvgAmount float64 `json:"avg_amount"`
}
// ScenarioSummary represents spending by category (scenario)
type ScenarioSummary struct {
CategoryID uint `json:"category_id"`
CategoryName string `json:"category_name"`
TotalAmount float64 `json:"total_amount"`
Count int64 `json:"count"`
Frequency float64 `json:"frequency"` // transactions per day
}
// GetConsumptionHabits analyzes consumption habits
func (s *ReportService) GetConsumptionHabits(userID uint, startDate, endDate time.Time, currency *models.Currency) (*ConsumptionHabit, error) {
// Get peak hours
peakHoursRepo, err := s.reportRepo.GetSpendingByHour(userID, startDate, endDate, currency)
if err != nil {
return nil, fmt.Errorf("failed to get peak hours: %w", err)
}
// Convert repository types to service types
peakHours := make([]HourSummary, len(peakHoursRepo))
for i, h := range peakHoursRepo {
peakHours[i] = HourSummary{
Hour: h.Hour,
TotalAmount: h.TotalAmount,
Count: h.Count,
AvgAmount: h.AvgAmount,
}
}
// Get common scenarios (categories)
scenariosRepo, err := s.reportRepo.GetCommonScenarios(userID, startDate, endDate, currency)
if err != nil {
return nil, fmt.Errorf("failed to get common scenarios: %w", err)
}
// Calculate frequency (transactions per day)
days := endDate.Sub(startDate).Hours() / 24
if days < 1 {
days = 1
}
scenarios := make([]ScenarioSummary, len(scenariosRepo))
for i, sc := range scenariosRepo {
scenarios[i] = ScenarioSummary{
CategoryID: sc.CategoryID,
CategoryName: sc.CategoryName,
TotalAmount: sc.TotalAmount,
Count: sc.Count,
Frequency: float64(sc.Count) / days,
}
}
return &ConsumptionHabit{
PeakHours: peakHours,
CommonScenarios: scenarios,
}, nil
}
// AssetLiabilityAnalysis represents asset and liability analysis
type AssetLiabilityAnalysis struct {
TotalAssets float64 `json:"total_assets"`
TotalLiabilities float64 `json:"total_liabilities"`
NetAssets float64 `json:"net_assets"`
AssetAccounts []AccountSummary `json:"asset_accounts"`
LiabilityAccounts []AccountSummary `json:"liability_accounts"`
AssetTrend []AssetTrendPoint `json:"asset_trend,omitempty"`
}
// AccountSummary represents account summary for asset/liability analysis
type AccountSummary struct {
AccountID uint `json:"account_id"`
AccountName string `json:"account_name"`
AccountType models.AccountType `json:"account_type"`
Balance float64 `json:"balance"`
Currency models.Currency `json:"currency"`
Percentage float64 `json:"percentage"`
}
// AssetTrendPoint represents a point in asset trend
type AssetTrendPoint struct {
Date time.Time `json:"date"`
TotalAssets float64 `json:"total_assets"`
TotalLiabilities float64 `json:"total_liabilities"`
NetAssets float64 `json:"net_assets"`
}
// GetAssetLiabilityAnalysis gets asset and liability analysis
func (s *ReportService) GetAssetLiabilityAnalysis(userID uint, includeTrend bool, trendStartDate, trendEndDate *time.Time) (*AssetLiabilityAnalysis, error) {
// Get all accounts
accounts, err := s.reportRepo.GetAllAccounts(userID)
if err != nil {
return nil, fmt.Errorf("failed to get accounts: %w", err)
}
result := &AssetLiabilityAnalysis{
AssetAccounts: make([]AccountSummary, 0),
LiabilityAccounts: make([]AccountSummary, 0),
}
// Separate assets and liabilities
for _, account := range accounts {
accountSummary := AccountSummary{
AccountID: account.ID,
AccountName: account.Name,
AccountType: account.Type,
Balance: account.Balance,
Currency: account.Currency,
}
if account.Balance >= 0 {
result.TotalAssets += account.Balance
result.AssetAccounts = append(result.AssetAccounts, accountSummary)
} else {
result.TotalLiabilities += -account.Balance
result.LiabilityAccounts = append(result.LiabilityAccounts, accountSummary)
}
}
result.NetAssets = result.TotalAssets - result.TotalLiabilities
// Calculate percentages
for i := range result.AssetAccounts {
if result.TotalAssets > 0 {
result.AssetAccounts[i].Percentage = (result.AssetAccounts[i].Balance / result.TotalAssets) * 100
}
}
for i := range result.LiabilityAccounts {
if result.TotalLiabilities > 0 {
result.LiabilityAccounts[i].Percentage = (-result.LiabilityAccounts[i].Balance / result.TotalLiabilities) * 100
}
}
// Get asset trend if requested
if includeTrend && trendStartDate != nil && trendEndDate != nil {
trendRepo, err := s.reportRepo.GetAssetTrend(userID, *trendStartDate, *trendEndDate)
if err != nil {
return nil, fmt.Errorf("failed to get asset trend: %w", err)
}
// Convert repository types to service types
trend := make([]AssetTrendPoint, len(trendRepo))
for i, t := range trendRepo {
trend[i] = AssetTrendPoint{
Date: t.Date,
TotalAssets: t.TotalAssets,
TotalLiabilities: t.TotalLiabilities,
NetAssets: t.NetAssets,
}
}
result.AssetTrend = trend
}
return result, nil
}