mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools
Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
* GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
* GET /api/v1/users/me/organizations - User organizations
* POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue
API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules
Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
475 lines
15 KiB
Go
475 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"`
|
|
}
|
|
|
|
// 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
|
|
|
|
// 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
|
|
}
|