turash/bugulma/backend/internal/matching/service.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

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