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)
293 lines
9.2 KiB
Go
293 lines
9.2 KiB
Go
package matching
|
|
|
|
import (
|
|
"context"
|
|
|
|
"bugulma/backend/internal/analysis/regulatory"
|
|
"bugulma/backend/internal/analysis/risk"
|
|
"bugulma/backend/internal/analysis/transport"
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/geospatial"
|
|
"bugulma/backend/internal/matching/engine"
|
|
"bugulma/backend/internal/matching/manager"
|
|
"bugulma/backend/internal/matching/plugins"
|
|
)
|
|
|
|
// Service orchestrates all matching functionality
|
|
type Service struct {
|
|
engine *engine.Engine
|
|
manager *manager.Manager
|
|
pluginMgr *plugins.Manager
|
|
matchRepo domain.MatchRepository
|
|
|
|
// Dependencies for data access
|
|
resourceFlowRepo domain.ResourceFlowRepository
|
|
siteRepo domain.SiteRepository
|
|
orgRepo domain.OrganizationRepository
|
|
eventBus domain.EventBus
|
|
|
|
// Geospatial calculator
|
|
geoCalc geospatial.Calculator
|
|
}
|
|
|
|
// EventBus is now defined in the domain package
|
|
|
|
// NewService creates a new matching service
|
|
func NewService(
|
|
matchRepo domain.MatchRepository,
|
|
negotiationRepo domain.NegotiationHistoryRepository,
|
|
resourceFlowRepo domain.ResourceFlowRepository,
|
|
siteRepo domain.SiteRepository,
|
|
orgRepo domain.OrganizationRepository,
|
|
riskSvc *risk.Service,
|
|
transportSvc *transport.Service,
|
|
regulatorySvc *regulatory.Service,
|
|
eventBus domain.EventBus,
|
|
) *Service {
|
|
|
|
// Create engine with analysis services
|
|
eng := engine.NewEngine(riskSvc, transportSvc, regulatorySvc)
|
|
|
|
// Create manager for lifecycle operations
|
|
mgr := manager.NewManager(matchRepo, negotiationRepo, eventBus)
|
|
|
|
// Create plugin registry and register default plugins
|
|
pluginRegistry := plugins.NewRegistry()
|
|
if err := plugins.RegisterDefaultPlugins(pluginRegistry); err != nil {
|
|
// Log error but continue - plugins are optional enhancement
|
|
// In production, this would be logged properly
|
|
}
|
|
|
|
// Create plugin manager
|
|
pluginMgr := plugins.NewManager(pluginRegistry, eng)
|
|
|
|
// Create geospatial calculator
|
|
geoCalc := geospatial.NewCalculatorWithDefaults()
|
|
|
|
return &Service{
|
|
engine: eng,
|
|
manager: mgr,
|
|
pluginMgr: pluginMgr,
|
|
matchRepo: matchRepo,
|
|
resourceFlowRepo: resourceFlowRepo,
|
|
siteRepo: siteRepo,
|
|
orgRepo: orgRepo,
|
|
eventBus: eventBus,
|
|
geoCalc: geoCalc,
|
|
}
|
|
}
|
|
|
|
// FindMatches finds potential matches based on criteria
|
|
func (s *Service) FindMatches(ctx context.Context, criteria Criteria) ([]*engine.Candidate, error) {
|
|
// Find potential source and target flows
|
|
sourceFlows, err := s.findSourceFlows(ctx, criteria)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targetFlows, err := s.findTargetFlows(ctx, criteria)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Use engine to find initial matches
|
|
candidates, err := s.engine.FindMatches(criteria, sourceFlows, targetFlows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Enhance candidates with plugin logic
|
|
var enhancedCandidates []*engine.Candidate
|
|
for _, candidate := range candidates {
|
|
// Calculate accurate distance using geospatial calculator
|
|
sourceSite, err := s.siteRepo.GetByID(ctx, candidate.SourceFlow.SiteID)
|
|
if err != nil {
|
|
continue // Skip if we can't get site info
|
|
}
|
|
targetSite, err := s.siteRepo.GetByID(ctx, candidate.TargetFlow.SiteID)
|
|
if err != nil {
|
|
continue // Skip if we can't get site info
|
|
}
|
|
|
|
distanceResult, err := s.geoCalc.CalculateDistance(
|
|
geospatial.Point{Latitude: sourceSite.Latitude, Longitude: sourceSite.Longitude},
|
|
geospatial.Point{Latitude: targetSite.Latitude, Longitude: targetSite.Longitude},
|
|
)
|
|
if err != nil {
|
|
continue // Skip if distance calculation fails
|
|
}
|
|
|
|
candidate.DistanceKm = distanceResult.DistanceKm
|
|
|
|
// Enhance candidate with plugin logic
|
|
if err := s.pluginMgr.EnhanceCandidate(candidate); err != nil {
|
|
// Log error but continue with basic candidate
|
|
continue
|
|
}
|
|
|
|
// Validate candidate with plugins
|
|
if valid, _ := s.pluginMgr.ValidateWithPlugins(candidate); valid {
|
|
enhancedCandidates = append(enhancedCandidates, candidate)
|
|
}
|
|
}
|
|
|
|
// Re-rank enhanced candidates
|
|
enhancedCandidates = s.rerankCandidates(enhancedCandidates)
|
|
|
|
return enhancedCandidates, nil
|
|
}
|
|
|
|
// CreateMatch creates a new match from a candidate
|
|
func (s *Service) CreateMatch(ctx context.Context, candidate *engine.Candidate, creatorID string) (*domain.Match, error) {
|
|
return s.manager.CreateMatch(ctx, candidate, creatorID)
|
|
}
|
|
|
|
// UpdateMatchStatus updates match status
|
|
func (s *Service) UpdateMatchStatus(ctx context.Context, matchID string, newStatus domain.MatchStatus, actorID string, notes string) error {
|
|
return s.manager.UpdateMatchStatus(ctx, matchID, newStatus, actorID, notes)
|
|
}
|
|
|
|
// UpdateMatch updates match details
|
|
func (s *Service) UpdateMatch(ctx context.Context, match *domain.Match) error {
|
|
return s.manager.UpdateMatch(ctx, match)
|
|
}
|
|
|
|
// DeleteMatch deletes a match
|
|
func (s *Service) DeleteMatch(ctx context.Context, matchID string, actorID string) error {
|
|
return s.manager.DeleteMatch(ctx, matchID, actorID)
|
|
}
|
|
|
|
// GetMatchByID retrieves a match by ID
|
|
func (s *Service) GetMatchByID(ctx context.Context, matchID string) (*domain.Match, error) {
|
|
return s.matchRepo.GetByID(ctx, matchID)
|
|
}
|
|
|
|
// GetTopMatches retrieves top matches
|
|
func (s *Service) GetTopMatches(ctx context.Context, limit int) ([]*domain.Match, error) {
|
|
return s.matchRepo.GetTopMatches(ctx, limit)
|
|
}
|
|
|
|
// GetNegotiationHistory retrieves negotiation history for a match
|
|
func (s *Service) GetNegotiationHistory(ctx context.Context, matchID string) ([]*domain.NegotiationHistoryEntry, error) {
|
|
return s.manager.GetNegotiationHistory(ctx, matchID)
|
|
}
|
|
|
|
// GetResourceFlowByID retrieves a resource flow by ID
|
|
func (s *Service) GetResourceFlowByID(ctx context.Context, id string) (*domain.ResourceFlow, error) {
|
|
return s.resourceFlowRepo.GetByID(ctx, id)
|
|
}
|
|
|
|
// GetSiteByID retrieves a site by ID
|
|
func (s *Service) GetSiteByID(ctx context.Context, id string) (*domain.Site, error) {
|
|
return s.siteRepo.GetByID(ctx, id)
|
|
}
|
|
|
|
// CalculateDistance calculates distance between two sites
|
|
func (s *Service) CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
|
|
result, err := s.geoCalc.CalculateDistance(
|
|
geospatial.Point{Latitude: lat1, Longitude: lon1},
|
|
geospatial.Point{Latitude: lat2, Longitude: lon2},
|
|
)
|
|
if err != nil {
|
|
// Fallback to 0 if calculation fails (shouldn't happen with valid coordinates)
|
|
return 0
|
|
}
|
|
return result.DistanceKm
|
|
}
|
|
|
|
// findSourceFlows finds resource flows that can be sources (outputs)
|
|
func (s *Service) findSourceFlows(ctx context.Context, criteria Criteria) ([]*domain.ResourceFlow, error) {
|
|
// For output matching, we look for flows with opposite direction
|
|
oppositeDirection := s.getOppositeDirection(criteria.ResourceType)
|
|
|
|
flows, err := s.resourceFlowRepo.GetByTypeAndDirection(ctx, criteria.ResourceType, oppositeDirection)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter by organization if specified
|
|
if criteria.OrganizationID != "" {
|
|
var filtered []*domain.ResourceFlow
|
|
for _, flow := range flows {
|
|
if flow.OrganizationID == criteria.OrganizationID {
|
|
filtered = append(filtered, flow)
|
|
}
|
|
}
|
|
flows = filtered
|
|
}
|
|
|
|
return flows, nil
|
|
}
|
|
|
|
// findTargetFlows finds resource flows that can be targets (inputs)
|
|
func (s *Service) findTargetFlows(ctx context.Context, criteria Criteria) ([]*domain.ResourceFlow, error) {
|
|
// For input matching, we look for flows in the specified direction
|
|
flows, err := s.resourceFlowRepo.GetByTypeAndDirection(ctx, criteria.ResourceType, criteria.Direction)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply geographic filtering if site specified
|
|
if criteria.SiteID != "" {
|
|
flows = s.filterBySiteProximity(ctx, flows, criteria.SiteID, criteria.MaxDistanceKm)
|
|
}
|
|
|
|
return flows, nil
|
|
}
|
|
|
|
// filterBySiteProximity filters flows by distance from a reference site
|
|
func (s *Service) filterBySiteProximity(ctx context.Context, flows []*domain.ResourceFlow, siteID string, maxDistance float64) []*domain.ResourceFlow {
|
|
if maxDistance <= 0 {
|
|
return flows
|
|
}
|
|
|
|
refSite, err := s.siteRepo.GetByID(ctx, siteID)
|
|
if err != nil {
|
|
return flows // Return all if we can't get reference site
|
|
}
|
|
|
|
var filtered []*domain.ResourceFlow
|
|
for _, flow := range flows {
|
|
flowSite, err := s.siteRepo.GetByID(ctx, flow.SiteID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
result, err := s.geoCalc.CalculateDistance(
|
|
geospatial.Point{Latitude: refSite.Latitude, Longitude: refSite.Longitude},
|
|
geospatial.Point{Latitude: flowSite.Latitude, Longitude: flowSite.Longitude},
|
|
)
|
|
if err != nil {
|
|
continue // Skip if distance calculation fails
|
|
}
|
|
distance := result.DistanceKm
|
|
|
|
if distance <= maxDistance {
|
|
filtered = append(filtered, flow)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// getOppositeDirection returns the opposite resource direction
|
|
func (s *Service) getOppositeDirection(resourceType domain.ResourceType) domain.ResourceDirection {
|
|
return domain.DirectionOutput // Simplified - would depend on matching strategy
|
|
}
|
|
|
|
// rerankCandidates sorts candidates by enhanced overall score
|
|
func (s *Service) rerankCandidates(candidates []*engine.Candidate) []*engine.Candidate {
|
|
// Simple sort by overall score (descending)
|
|
for i := 0; i < len(candidates); i++ {
|
|
for j := i + 1; j < len(candidates); j++ {
|
|
if candidates[i].OverallScore < candidates[j].OverallScore {
|
|
candidates[i], candidates[j] = candidates[j], candidates[i]
|
|
}
|
|
}
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// Import Criteria from engine package
|
|
type Criteria = engine.Criteria
|