turash/bugulma/backend/internal/matching/service.go

606 lines
18 KiB
Go

package matching
import (
"context"
"fmt"
"strings"
"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
productRepo domain.ProductRepository
serviceRepo domain.ServiceRepository
communityListingRepo domain.CommunityListingRepository
eventBus domain.EventBus
// Geospatial calculator
geoCalc geospatial.Calculator
// Discovery matcher for products/services
discoveryMatcher *DiscoveryMatcher
}
// 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,
productRepo domain.ProductRepository,
serviceRepo domain.ServiceRepository,
communityListingRepo domain.CommunityListingRepository,
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()
// Create discovery matcher for products/services
discoveryMatcher := NewDiscoveryMatcher()
return &Service{
engine: eng,
manager: mgr,
pluginMgr: pluginMgr,
matchRepo: matchRepo,
resourceFlowRepo: resourceFlowRepo,
siteRepo: siteRepo,
orgRepo: orgRepo,
productRepo: productRepo,
serviceRepo: serviceRepo,
communityListingRepo: communityListingRepo,
eventBus: eventBus,
geoCalc: geoCalc,
discoveryMatcher: discoveryMatcher,
}
}
// 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
// FindProductMatches finds products matching the discovery query
func (s *Service) FindProductMatches(ctx context.Context, query DiscoveryQuery) ([]*DiscoveryMatch, error) {
if s.productRepo == nil {
return nil, fmt.Errorf("product repository not available")
}
// Search products based on query
var products []*domain.Product
var err error
if query.Location != nil && query.RadiusKm > 0 {
products, err = s.productRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm)
} else {
products, err = s.productRepo.GetAll(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to fetch products: %w", err)
}
// Score and rank matches
var matches []*DiscoveryMatch
for _, product := range products {
// Get organization and site
var org *domain.Organization
var site *domain.Site
if product.OrganizationID != "" {
org, _ = s.orgRepo.GetByID(ctx, product.OrganizationID)
}
if product.SiteID != nil {
site, _ = s.siteRepo.GetByID(ctx, *product.SiteID)
}
match, err := s.discoveryMatcher.ScoreProductMatch(product, query, org, site)
if err != nil {
continue
}
// Apply filters
if query.AvailabilityStatus != "" && product.AvailabilityStatus != query.AvailabilityStatus {
continue
}
if len(query.Categories) > 0 {
categoryMatched := false
for _, cat := range query.Categories {
if strings.EqualFold(cat, string(product.Category)) {
categoryMatched = true
break
}
}
if !categoryMatched {
continue
}
}
matches = append(matches, match)
}
// Sort by relevance score
for i := 0; i < len(matches); i++ {
for j := i + 1; j < len(matches); j++ {
if matches[i].RelevanceScore < matches[j].RelevanceScore {
matches[i], matches[j] = matches[j], matches[i]
}
}
}
// Apply pagination
start := query.Offset
end := start + query.Limit
if start >= len(matches) {
return []*DiscoveryMatch{}, nil
}
if end > len(matches) {
end = len(matches)
}
return matches[start:end], nil
}
// FindServiceMatches finds services matching the discovery query
func (s *Service) FindServiceMatches(ctx context.Context, query DiscoveryQuery) ([]*DiscoveryMatch, error) {
if s.serviceRepo == nil {
return nil, fmt.Errorf("service repository not available")
}
// Search services based on query
var services []*domain.Service
var err error
if query.Location != nil && query.RadiusKm > 0 {
services, err = s.serviceRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm)
} else {
services, err = s.serviceRepo.GetAll(ctx)
}
if err != nil {
return nil, fmt.Errorf("failed to fetch services: %w", err)
}
// Score and rank matches
var matches []*DiscoveryMatch
for _, service := range services {
// Get organization and site
var org *domain.Organization
var site *domain.Site
if service.OrganizationID != "" {
org, _ = s.orgRepo.GetByID(ctx, service.OrganizationID)
}
if service.SiteID != nil {
site, _ = s.siteRepo.GetByID(ctx, *service.SiteID)
}
match, err := s.discoveryMatcher.ScoreServiceMatch(service, query, org, site)
if err != nil {
continue
}
// Apply filters
if query.AvailabilityStatus != "" && service.AvailabilityStatus != query.AvailabilityStatus {
continue
}
matches = append(matches, match)
}
// Sort by relevance score
for i := 0; i < len(matches); i++ {
for j := i + 1; j < len(matches); j++ {
if matches[i].RelevanceScore < matches[j].RelevanceScore {
matches[i], matches[j] = matches[j], matches[i]
}
}
}
// Apply pagination
start := query.Offset
end := start + query.Limit
if start >= len(matches) {
return []*DiscoveryMatch{}, nil
}
if end > len(matches) {
end = len(matches)
}
return matches[start:end], nil
}
// UniversalSearch performs a unified search across resources, products, services, and community listings
func (s *Service) UniversalSearch(ctx context.Context, query DiscoveryQuery) (*UniversalSearchResult, error) {
result := &UniversalSearchResult{
Query: query,
}
// Search products (soft match)
if productMatches, err := s.FindProductMatches(ctx, query); err == nil {
result.ProductMatches = productMatches
}
// Search services (soft match)
if serviceMatches, err := s.FindServiceMatches(ctx, query); err == nil {
result.ServiceMatches = serviceMatches
}
// Search community listings (soft match)
if s.communityListingRepo != nil {
var listings []*domain.CommunityListing
var err error
if query.Location != nil && query.RadiusKm > 0 {
listings, err = s.communityListingRepo.GetNearby(ctx, query.Location.Latitude, query.Location.Longitude, query.RadiusKm)
} else {
listings, err = s.communityListingRepo.GetAll(ctx)
}
if err == nil {
for _, listing := range listings {
match, err := s.discoveryMatcher.ScoreCommunityListingMatch(listing, query)
if err == nil {
result.CommunityMatches = append(result.CommunityMatches, match)
}
}
// Sort community matches
for i := 0; i < len(result.CommunityMatches); i++ {
for j := i + 1; j < len(result.CommunityMatches); j++ {
if result.CommunityMatches[i].RelevanceScore < result.CommunityMatches[j].RelevanceScore {
result.CommunityMatches[i], result.CommunityMatches[j] = result.CommunityMatches[j], result.CommunityMatches[i]
}
}
}
}
}
// Note: Resource flow matches (hard match) would be handled separately via FindMatches
// This is the "soft match" layer as per concept's layered architecture
return result, nil
}
// UniversalSearchResult contains results from universal search
type UniversalSearchResult struct {
Query DiscoveryQuery `json:"query"`
ProductMatches []*DiscoveryMatch `json:"product_matches"`
ServiceMatches []*DiscoveryMatch `json:"service_matches"`
CommunityMatches []*DiscoveryMatch `json:"community_matches"`
// ResourceMatches would be added via separate FindMatches call (hard match)
}
// GetProductsByOrganization gets products for a specific organization (efficient method)
func (s *Service) GetProductsByOrganization(ctx context.Context, organizationID string) ([]*domain.Product, error) {
if s.productRepo == nil {
return nil, fmt.Errorf("product repository not available")
}
return s.productRepo.GetByOrganization(ctx, organizationID)
}
// GetServicesByOrganization gets services for a specific organization (efficient method)
func (s *Service) GetServicesByOrganization(ctx context.Context, organizationID string) ([]*domain.Service, error) {
if s.serviceRepo == nil {
return nil, fmt.Errorf("service repository not available")
}
return s.serviceRepo.GetByOrganization(ctx, organizationID)
}
// CreateProduct creates a new product with site linking support
func (s *Service) CreateProduct(ctx context.Context, product *domain.Product) error {
if s.productRepo == nil {
return fmt.Errorf("product repository not available")
}
// If SiteID is provided, populate location from site
if product.SiteID != nil && *product.SiteID != "" {
site, err := s.siteRepo.GetByID(ctx, *product.SiteID)
if err == nil && site != nil {
// Set location from site coordinates
product.Location = domain.Point{
Latitude: site.Latitude,
Longitude: site.Longitude,
Valid: true,
}
}
}
if err := s.productRepo.Create(ctx, product); err != nil {
return err
}
// Sync to graph database if graph sync service is available
// Note: This would require passing graph sync service to matching service
// For now, we'll rely on event-driven sync
return nil
}
// CreateService creates a new service with site linking support
func (s *Service) CreateService(ctx context.Context, service *domain.Service) error {
if s.serviceRepo == nil {
return fmt.Errorf("service repository not available")
}
// If SiteID is provided, populate location from site
if service.SiteID != nil && *service.SiteID != "" {
site, err := s.siteRepo.GetByID(ctx, *service.SiteID)
if err == nil && site != nil {
// Set location from site coordinates
service.ServiceLocation = domain.Point{
Latitude: site.Latitude,
Longitude: site.Longitude,
Valid: true,
}
}
}
if err := s.serviceRepo.Create(ctx, service); err != nil {
return err
}
return nil
}
// CreateCommunityListing creates a new community listing
func (s *Service) CreateCommunityListing(ctx context.Context, listing *domain.CommunityListing) error {
if s.communityListingRepo == nil {
return fmt.Errorf("community listing repository not available")
}
// Validate the listing
if err := listing.Validate(); err != nil {
return fmt.Errorf("invalid listing: %w", err)
}
// Create the listing
if err := s.communityListingRepo.Create(ctx, listing); err != nil {
return fmt.Errorf("failed to create community listing: %w", err)
}
return nil
}