turash/bugulma/backend/internal/graph/traversal.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
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)
2025-11-25 06:01:16 +01:00

240 lines
7.4 KiB
Go

package graph
import (
"context"
"fmt"
"bugulma/backend/internal/domain"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
// Traversal handles graph traversal operations
type Traversal struct {
sessionManager *SessionManager
queryTemplates *QueryTemplates
nodeConverter *NodeConverter
config *Config
}
// NewTraversal creates a new traversal instance
func NewTraversal(sessionManager *SessionManager, queryTemplates *QueryTemplates, nodeConverter *NodeConverter, config *Config) *Traversal {
return &Traversal{
sessionManager: sessionManager,
queryTemplates: queryTemplates,
nodeConverter: nodeConverter,
config: config,
}
}
// FindResourceChains finds chains of resource flows from waste to reuse
func (t *Traversal) FindResourceChains(
ctx context.Context,
startResourceType domain.ResourceType,
maxChainLength int,
minValue float64,
) ([]*ResourceChain, error) {
if maxChainLength > t.config.MaxChainLength {
maxChainLength = t.config.MaxChainLength
}
if minValue < t.config.MinChainValue {
minValue = t.config.MinChainValue
}
result, err := t.sessionManager.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := t.queryTemplates.FindResourceChainsQuery(maxChainLength)
queryResult, err := tx.Run(ctx, cypher, map[string]interface{}{
"resource_type": string(startResourceType),
"min_value": minValue,
"max_distance": t.config.MaxChainDistanceKm,
"limit": t.config.DefaultResultLimit,
})
if err != nil {
return nil, fmt.Errorf("failed to execute resource chain query: %w", err)
}
var chains []*ResourceChain
for queryResult.Next(ctx) {
record := queryResult.Record()
pathNodes, _ := record.Get("pathNodes")
pathRels, _ := record.Get("pathRels")
totalDistance, _ := record.Get("total_distance")
totalValue, _ := record.Get("total_value")
startOrgID, _ := record.Get("start_org_id")
startOrgName, _ := record.Get("start_org_name")
endOrgID, _ := record.Get("end_org_id")
endOrgName, _ := record.Get("end_org_name")
chain := &ResourceChain{
ChainID: fmt.Sprintf("chain_%s_%d", startResourceType, len(chains)+1),
ResourceType: startResourceType,
TotalDistanceKm: getFloat64Value(totalDistance),
TotalCost: getFloat64Value(totalValue),
EnvironmentalImpact: getFloat64Value(totalValue) * 0.3,
Circular: false,
Steps: []ResourceChainStep{},
}
// Build chain steps from path nodes and relationships
if nodesSlice, ok := pathNodes.([]interface{}); ok {
if relsSlice, ok := pathRels.([]interface{}); ok {
for i := 0; i < len(nodesSlice)-1 && i < len(relsSlice); i++ {
sourceNode := nodesSlice[i].(neo4j.Node)
targetNode := nodesSlice[i+1].(neo4j.Node)
rel := relsSlice[i].(neo4j.Relationship)
step := ResourceChainStep{
StepNumber: i + 1,
SourceFlowID: getStringProp(sourceNode.Props, "id"),
TargetFlowID: getStringProp(targetNode.Props, "id"),
SourceOrgID: getStringValue(startOrgID),
SourceOrgName: getStringValue(startOrgName),
TargetOrgID: getStringValue(endOrgID),
TargetOrgName: getStringValue(endOrgName),
DistanceKm: getFloat64Prop(rel.Props, "distance_km"),
TransportCost: getFloat64Prop(rel.Props, "transport_cost"),
ProcessingCost: getFloat64Prop(rel.Props, "processing_cost"),
ResourceQuantity: getFloat64Prop(sourceNode.Props, "quantity"),
}
chain.Steps = append(chain.Steps, step)
}
}
}
chains = append(chains, chain)
}
if err := queryResult.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate chain results: %w", err)
}
return chains, nil
})
if err != nil {
return nil, err
}
return result.([]*ResourceChain), nil
}
// FindSymbiosisNetworks identifies interconnected industrial symbiosis networks
func (t *Traversal) FindSymbiosisNetworks(
ctx context.Context,
minOrganizations int,
maxNetworkSize int,
) ([]*SymbiosisNetwork, error) {
if minOrganizations < t.config.MinNetworkSize {
minOrganizations = t.config.MinNetworkSize
}
if maxNetworkSize > t.config.MaxNetworkSize {
maxNetworkSize = t.config.MaxNetworkSize
}
result, err := t.sessionManager.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := t.queryTemplates.FindSymbiosisNetworksQuery()
queryResult, err := tx.Run(ctx, cypher, map[string]interface{}{
"min_orgs": minOrganizations,
"max_size": maxNetworkSize,
"limit": t.config.DefaultResultLimit,
})
if err != nil {
return nil, fmt.Errorf("failed to execute symbiosis network query: %w", err)
}
var networks []*SymbiosisNetwork
for queryResult.Next(ctx) {
record := queryResult.Record()
networkOrgs, _ := record.Get("network_orgs")
connections, _ := record.Get("connections")
totalValue, _ := record.Get("total_value")
maxDistance, _ := record.Get("max_distance")
network := &SymbiosisNetwork{
NetworkID: fmt.Sprintf("network_%d", len(networks)+1),
TotalValue: getFloat64Value(totalValue),
GeographicSpan: getFloat64Value(maxDistance),
NetworkEfficiency: 0.85,
EnvironmentalSavings: getFloat64Value(totalValue) * 0.2,
Organizations: []NetworkOrganization{},
ResourceFlows: []NetworkResourceFlow{},
}
// Process organizations
if orgsSlice, ok := networkOrgs.([]interface{}); ok {
for _, orgID := range orgsSlice {
if orgIDStr, ok := orgID.(string); ok {
orgNode, err := t.getOrganizationNode(ctx, tx, orgIDStr)
if err == nil {
props := orgNode.Props
networkOrg := NetworkOrganization{
OrganizationID: orgIDStr,
Name: getStringProp(props, "name"),
Role: "participant",
Latitude: getFloat64Prop(props, "latitude"),
Longitude: getFloat64Prop(props, "longitude"),
}
network.Organizations = append(network.Organizations, networkOrg)
}
}
}
}
// Process resource flows
if connsSlice, ok := connections.([]interface{}); ok {
for _, conn := range connsSlice {
if connMap, ok := conn.(map[string]interface{}); ok {
networkFlow := NetworkResourceFlow{
FlowID: getStringProp(connMap, "flow_id"),
ResourceType: getStringProp(connMap, "resource_type"),
Value: getFloat64Prop(connMap, "economic_value"),
DistanceKm: getFloat64Prop(connMap, "distance"),
}
network.ResourceFlows = append(network.ResourceFlows, networkFlow)
}
}
}
networks = append(networks, network)
}
if err := queryResult.Err(); err != nil {
return nil, fmt.Errorf("failed to iterate network results: %w", err)
}
return networks, nil
})
if err != nil {
return nil, err
}
return result.([]*SymbiosisNetwork), nil
}
// getOrganizationNode retrieves an organization node by ID
func (t *Traversal) getOrganizationNode(ctx context.Context, tx neo4j.ManagedTransaction, orgID string) (neo4j.Node, error) {
cypher := `MATCH (o:Organization {id: $org_id}) RETURN o LIMIT 1`
result, err := tx.Run(ctx, cypher, map[string]interface{}{"org_id": orgID})
if err != nil {
return neo4j.Node{}, err
}
if result.Next(ctx) {
record := result.Record()
if node, ok := record.Get("o"); ok {
if neo4jNode, ok := node.(neo4j.Node); ok {
return neo4jNode, nil
}
}
}
return neo4j.Node{}, fmt.Errorf("organization not found: %s", orgID)
}