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)
240 lines
7.4 KiB
Go
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)
|
|
}
|