turash/bugulma/backend/internal/service/analytics_service.go
2025-12-15 10:06:41 +01:00

483 lines
15 KiB
Go

package service
import (
"bugulma/backend/internal/domain"
"context"
"fmt"
"gorm.io/gorm"
)
// AnalyticsService provides platform analytics and statistics
type AnalyticsService struct {
db *gorm.DB
orgRepo domain.OrganizationRepository
siteRepo domain.SiteRepository
resourceFlowRepo domain.ResourceFlowRepository
matchRepo domain.MatchRepository
sharedAssetRepo domain.SharedAssetRepository
}
// NewAnalyticsService creates a new analytics service
func NewAnalyticsService(
db *gorm.DB,
orgRepo domain.OrganizationRepository,
siteRepo domain.SiteRepository,
resourceFlowRepo domain.ResourceFlowRepository,
matchRepo domain.MatchRepository,
sharedAssetRepo domain.SharedAssetRepository,
) *AnalyticsService {
return &AnalyticsService{
db: db,
orgRepo: orgRepo,
siteRepo: siteRepo,
resourceFlowRepo: resourceFlowRepo,
matchRepo: matchRepo,
sharedAssetRepo: sharedAssetRepo,
}
}
// PlatformStatistics represents overall platform metrics
type PlatformStatistics struct {
TotalOrganizations int `json:"total_organizations"`
TotalSites int `json:"total_sites"`
TotalResourceFlows int `json:"total_resource_flows"`
TotalMatches int `json:"total_matches"`
TotalSharedAssets int `json:"total_shared_assets"`
ActiveMatches int `json:"active_matches"`
CO2SavingsTonnes float64 `json:"co2_savings_tonnes"`
EconomicValueEUR float64 `json:"economic_value_eur"`
}
// OrganizationStatistics represents statistics for a single organization
type OrganizationStatistics struct {
OrganizationID string `json:"organization_id"`
TotalSites int `json:"total_sites"`
TotalResourceFlows int `json:"total_resource_flows"`
ActiveMatches int `json:"active_matches"`
TotalMatches int `json:"total_matches"`
CO2SavingsTonnes float64 `json:"co2_savings_tonnes"`
EconomicValueEUR float64 `json:"economic_value_eur"`
}
// MatchingStatistics represents matching engine performance metrics
type MatchingStatistics struct {
TotalMatches int `json:"total_matches"`
ActiveMatches int `json:"active_matches"`
CompletedMatches int `json:"completed_matches"`
AvgCompatibilityScore float64 `json:"avg_compatibility_score"`
AvgEconomicValue float64 `json:"avg_economic_value"`
AvgDistanceKm float64 `json:"avg_distance_km"`
}
// ResourceFlowStatistics represents resource flow metrics
type ResourceFlowStatistics struct {
TotalFlows int `json:"total_flows"`
InputFlows int `json:"input_flows"`
OutputFlows int `json:"output_flows"`
FlowsByType map[string]int `json:"flows_by_type"`
FlowsByDirection map[string]int `json:"flows_by_direction"`
}
// ImpactMetrics represents environmental and economic impact
type ImpactMetrics struct {
TotalCO2SavingsTonnes float64 `json:"total_co2_savings_tonnes"`
TotalEconomicValueEUR float64 `json:"total_economic_value_eur"`
MaterialsRecycledTonnes float64 `json:"materials_recycled_tonnes"`
EnergySharedMWh float64 `json:"energy_shared_mwh"`
WaterReclaimedM3 float64 `json:"water_reclaimed_m3"`
ActiveMatchesCount int `json:"active_matches_count"`
}
// DashboardStatistics represents dashboard overview statistics
type DashboardStatistics struct {
TotalOrganizations int `json:"total_organizations"`
TotalSites int `json:"total_sites"`
TotalResourceFlows int `json:"total_resource_flows"`
TotalMatches int `json:"total_matches"`
ActiveProposals int `json:"active_proposals"`
RecentActivity []ActivityItem `json:"recent_activity"`
}
// ActivityItem represents a recent activity entry
type ActivityItem struct {
ID string `json:"id"`
Type string `json:"type"`
Description string `json:"description"`
Timestamp string `json:"timestamp"`
}
// ConnectionStatistics represents connection and relationship statistics
type ConnectionStatistics struct {
TotalConnections int `json:"total_connections"`
ActiveConnections int `json:"active_connections"`
PotentialConnections int `json:"potential_connections"`
}
// SupplyDemandAnalysis represents supply and demand analysis
type SupplyDemandAnalysis struct {
TopNeeds []ItemCount `json:"top_needs"`
TopOffers []ItemCount `json:"top_offers"`
}
// ItemCount represents an item with its count
type ItemCount struct {
Item string `json:"item"`
Count int `json:"count"`
}
// GetPlatformStatistics retrieves overall platform statistics
func (s *AnalyticsService) GetPlatformStatistics(ctx context.Context) (*PlatformStatistics, error) {
stats := &PlatformStatistics{}
// Count organizations
var orgCount int64
if err := s.db.Model(&domain.Organization{}).Count(&orgCount).Error; err != nil {
return nil, err
}
stats.TotalOrganizations = int(orgCount)
// Count sites
var siteCount int64
if err := s.db.Model(&domain.Site{}).Count(&siteCount).Error; err != nil {
return nil, err
}
stats.TotalSites = int(siteCount)
// Count resource flows
var flowCount int64
if err := s.db.Model(&domain.ResourceFlow{}).Count(&flowCount).Error; err != nil {
return nil, err
}
stats.TotalResourceFlows = int(flowCount)
// Count matches
var matchCount int64
if err := s.db.Model(&domain.Match{}).Count(&matchCount).Error; err != nil {
return nil, err
}
stats.TotalMatches = int(matchCount)
// Count active matches
var activeMatchCount int64
if err := s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"suggested", "negotiating", "reserved", "contracted", "live"}).
Count(&activeMatchCount).Error; err != nil {
return nil, err
}
stats.ActiveMatches = int(activeMatchCount)
// Count shared assets
var assetCount int64
if err := s.db.Model(&domain.SharedAsset{}).Count(&assetCount).Error; err != nil {
return nil, err
}
stats.TotalSharedAssets = int(assetCount)
// Calculate aggregate impact metrics
var aggregates struct {
CO2Savings float64
EconomicValue float64
}
s.db.Model(&domain.Match{}).
Select("COALESCE(SUM(environmental_impact->>'co2_savings_kg'), 0) as co2_savings, COALESCE(SUM(economic_value), 0) as economic_value").
Where("status IN ?", []string{"contracted", "live"}).
Scan(&aggregates)
stats.CO2SavingsTonnes = aggregates.CO2Savings / 1000 // Convert kg to tonnes
stats.EconomicValueEUR = aggregates.EconomicValue
return stats, nil
}
// GetOrganizationStatistics retrieves statistics for a specific organization
func (s *AnalyticsService) GetOrganizationStatistics(ctx context.Context, orgID string) (*OrganizationStatistics, error) {
stats := &OrganizationStatistics{
OrganizationID: orgID,
}
// Count sites
var siteCount int64
if err := s.db.Model(&domain.Site{}).
Where("owner_organization_id = ?", orgID).
Count(&siteCount).Error; err != nil {
return nil, err
}
stats.TotalSites = int(siteCount)
// Count resource flows
var flowCount int64
if err := s.db.Model(&domain.ResourceFlow{}).
Where("organization_id = ?", orgID).
Count(&flowCount).Error; err != nil {
return nil, err
}
stats.TotalResourceFlows = int(flowCount)
// Count matches (source or target)
var matchCount int64
s.db.Model(&domain.Match{}).
Joins("JOIN resource_flows rf ON matches.source_resource_id = rf.id OR matches.target_resource_id = rf.id").
Where("rf.organization_id = ?", orgID).
Count(&matchCount)
stats.TotalMatches = int(matchCount)
// Count active matches
var activeMatchCount int64
s.db.Model(&domain.Match{}).
Joins("JOIN resource_flows rf ON matches.source_resource_id = rf.id OR matches.target_resource_id = rf.id").
Where("rf.organization_id = ? AND matches.status IN ?", orgID, []string{"suggested", "negotiating", "reserved", "contracted", "live"}).
Count(&activeMatchCount)
stats.ActiveMatches = int(activeMatchCount)
return stats, nil
}
// GetMatchingStatistics retrieves matching engine performance statistics
func (s *AnalyticsService) GetMatchingStatistics(ctx context.Context) (*MatchingStatistics, error) {
stats := &MatchingStatistics{}
// Total matches
var matchCount int64
if err := s.db.Model(&domain.Match{}).Count(&matchCount).Error; err != nil {
return nil, err
}
stats.TotalMatches = int(matchCount)
// Active matches
var activeCount int64
if err := s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"suggested", "negotiating", "reserved", "contracted", "live"}).
Count(&activeCount).Error; err != nil {
return nil, err
}
stats.ActiveMatches = int(activeCount)
// Completed matches
var completedCount int64
if err := s.db.Model(&domain.Match{}).
Where("status = ?", "completed").
Count(&completedCount).Error; err != nil {
return nil, err
}
stats.CompletedMatches = int(completedCount)
// Average metrics
var averages struct {
AvgScore float64
AvgValue float64
AvgDistance float64
}
s.db.Model(&domain.Match{}).
Select("AVG(compatibility_score) as avg_score, AVG(economic_value) as avg_value, AVG(distance_km) as avg_distance").
Scan(&averages)
stats.AvgCompatibilityScore = averages.AvgScore
stats.AvgEconomicValue = averages.AvgValue
stats.AvgDistanceKm = averages.AvgDistance
return stats, nil
}
// GetResourceFlowStatistics retrieves resource flow statistics
func (s *AnalyticsService) GetResourceFlowStatistics(ctx context.Context) (*ResourceFlowStatistics, error) {
stats := &ResourceFlowStatistics{
FlowsByType: make(map[string]int),
FlowsByDirection: make(map[string]int),
}
// Total flows
var flowCount int64
if err := s.db.Model(&domain.ResourceFlow{}).Count(&flowCount).Error; err != nil {
return nil, err
}
stats.TotalFlows = int(flowCount)
// Count by direction
var directionCounts []struct {
Direction string
Count int
}
s.db.Model(&domain.ResourceFlow{}).
Select("direction, COUNT(*) as count").
Group("direction").
Scan(&directionCounts)
for _, dc := range directionCounts {
stats.FlowsByDirection[dc.Direction] = dc.Count
if dc.Direction == "input" {
stats.InputFlows = dc.Count
} else if dc.Direction == "output" {
stats.OutputFlows = dc.Count
}
}
// Count by type
var typeCounts []struct {
Type string
Count int
}
s.db.Model(&domain.ResourceFlow{}).
Select("type, COUNT(*) as count").
Group("type").
Scan(&typeCounts)
for _, tc := range typeCounts {
stats.FlowsByType[tc.Type] = tc.Count
}
return stats, nil
}
// GetImpactMetrics retrieves environmental and economic impact metrics
func (s *AnalyticsService) GetImpactMetrics(ctx context.Context) (*ImpactMetrics, error) {
metrics := &ImpactMetrics{}
// Aggregate impact from active/completed matches
var aggregates struct {
CO2Savings float64
EconValue float64
}
s.db.Model(&domain.Match{}).
Select("COALESCE(SUM(environmental_impact->>'co2_savings_kg'), 0) as co2_savings, COALESCE(SUM(economic_value), 0) as econ_value").
Where("status IN ?", []string{"contracted", "live", "completed"}).
Scan(&aggregates)
metrics.TotalCO2SavingsTonnes = aggregates.CO2Savings / 1000
metrics.TotalEconomicValueEUR = aggregates.EconValue
// Count active matches
var activeMatchesCount int64
s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"suggested", "negotiating", "contracted", "live"}).
Count(&activeMatchesCount)
metrics.ActiveMatchesCount = int(activeMatchesCount)
// TODO: Calculate materials recycled, energy shared, water reclaimed from resource flows
// This would require additional analysis of resource flow quantity data
return metrics, nil
}
// GetDashboardStatistics retrieves dashboard overview statistics
func (s *AnalyticsService) GetDashboardStatistics(ctx context.Context) (*DashboardStatistics, error) {
stats := &DashboardStatistics{}
// Get basic counts
var orgCount, siteCount, flowCount, matchCount, activeProposalCount int64
s.db.Model(&domain.Organization{}).Count(&orgCount)
s.db.Model(&domain.Site{}).Count(&siteCount)
s.db.Model(&domain.ResourceFlow{}).Count(&flowCount)
s.db.Model(&domain.Match{}).Count(&matchCount)
// Active proposals (matches in early stages)
s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"suggested", "negotiating"}).
Count(&activeProposalCount)
stats.TotalOrganizations = int(orgCount)
stats.TotalSites = int(siteCount)
stats.TotalResourceFlows = int(flowCount)
stats.TotalMatches = int(matchCount)
stats.ActiveProposals = int(activeProposalCount)
// Recent activity (last 10 activities)
var recentMatches []domain.Match
s.db.Order("created_at DESC").
Limit(10).
Find(&recentMatches)
stats.RecentActivity = make([]ActivityItem, 0, len(recentMatches))
for _, match := range recentMatches {
description := fmt.Sprintf("Match created with compatibility score %.2f", match.CompatibilityScore)
stats.RecentActivity = append(stats.RecentActivity, ActivityItem{
ID: match.ID,
Type: "match",
Description: description,
Timestamp: match.CreatedAt.Format("2006-01-02T15:04:05Z"),
})
}
return stats, nil
}
// GetConnectionStatistics retrieves connection and relationship statistics
func (s *AnalyticsService) GetConnectionStatistics(ctx context.Context) (*ConnectionStatistics, error) {
stats := &ConnectionStatistics{}
// Total connections (active matches)
var totalConnections int64
s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"contracted", "live", "completed"}).
Count(&totalConnections)
stats.TotalConnections = int(totalConnections)
// Active connections (live matches)
var activeConnections int64
s.db.Model(&domain.Match{}).
Where("status = ?", "live").
Count(&activeConnections)
stats.ActiveConnections = int(activeConnections)
// Potential connections (suggested matches)
var potentialConnections int64
s.db.Model(&domain.Match{}).
Where("status IN ?", []string{"suggested", "negotiating"}).
Count(&potentialConnections)
stats.PotentialConnections = int(potentialConnections)
return stats, nil
}
// GetSupplyDemandAnalysis retrieves supply and demand analysis
func (s *AnalyticsService) GetSupplyDemandAnalysis(ctx context.Context) (*SupplyDemandAnalysis, error) {
analysis := &SupplyDemandAnalysis{
TopNeeds: []ItemCount{},
TopOffers: []ItemCount{},
}
// Top needs (most requested resource types)
var needCounts []struct {
ResourceType string
Count int
}
s.db.Model(&domain.ResourceFlow{}).
Select("resource_type, COUNT(*) as count").
Where("direction = ?", "input").
Group("resource_type").
Order("count DESC").
Limit(10).
Scan(&needCounts)
for _, nc := range needCounts {
analysis.TopNeeds = append(analysis.TopNeeds, ItemCount{
Item: nc.ResourceType,
Count: nc.Count,
})
}
// Top offers (most offered resource types)
var offerCounts []struct {
ResourceType string
Count int
}
s.db.Model(&domain.ResourceFlow{}).
Select("resource_type, COUNT(*) as count").
Where("direction = ?", "output").
Group("resource_type").
Order("count DESC").
Limit(10).
Scan(&offerCounts)
for _, oc := range offerCounts {
analysis.TopOffers = append(analysis.TopOffers, ItemCount{
Item: oc.ResourceType,
Count: oc.Count,
})
}
return analysis, nil
}