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

218 lines
6.2 KiB
Go

package plugins
import (
"fmt"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/matching/engine"
)
// Manager handles plugin-based matching logic integration
type Manager struct {
registry PluginRegistry
engine *engine.Engine
}
// NewManager creates a new plugin manager
func NewManager(registry PluginRegistry, engine *engine.Engine) *Manager {
return &Manager{
registry: registry,
engine: engine,
}
}
// EnhanceCandidate uses plugins to enhance candidate evaluation
func (pm *Manager) EnhanceCandidate(candidate *engine.Candidate) error {
sourceFlow := candidate.SourceFlow
targetFlow := candidate.TargetFlow
// Get plugins for this resource type
plugins := pm.registry.GetByResourceType(sourceFlow.Type)
if len(plugins) == 0 {
// No plugins available, use default engine logic
return nil
}
// Use the first available plugin (in production, you might want plugin selection logic)
plugin := plugins[0]
// Calculate distance (simplified - should use geospatial calculator)
distance := candidate.DistanceKm
if distance == 0 {
distance = 10.0 // Placeholder distance
}
// Apply plugin enhancements
if plugin.SupportsQualityCheck() {
if qualityScore, err := plugin.CheckQualityCompatibility(sourceFlow, targetFlow); err != nil {
return fmt.Errorf("quality check failed: %w", err)
} else {
// Blend plugin score with existing score
candidate.QualityScore = (candidate.QualityScore + qualityScore) / 2
}
}
if plugin.SupportsEconomicCalculation() {
if economicValue, err := plugin.CalculateEconomicImpact(sourceFlow, targetFlow, distance); err != nil {
// Log warning but don't fail - use existing calculation
fmt.Printf("Warning: Plugin economic calculation failed: %v\n", err)
} else {
// Use plugin's economic calculation if available
candidate.EstimatedAnnualSavings = economicValue
}
}
if plugin.SupportsTemporalCheck() {
if temporalScore, err := plugin.CheckTemporalCompatibility(sourceFlow, targetFlow); err != nil {
return fmt.Errorf("temporal check failed: %w", err)
} else {
// Blend plugin score with existing score
candidate.TemporalScore = (candidate.TemporalScore + temporalScore) / 2
}
}
// Recalculate overall score with enhanced components
scoring := &engine.ScoringResult{
Compatibility: candidate.CompatibilityScore,
Economic: pm.normalizeEconomicScore(candidate.EstimatedAnnualSavings),
Temporal: candidate.TemporalScore,
Quality: candidate.QualityScore,
}
// Simple weighted calculation (should match engine logic)
weights := map[string]float64{
"compatibility": 0.3,
"economic": 0.25,
"temporal": 0.25,
"quality": 0.2,
}
candidate.OverallScore = scoring.Compatibility*weights["compatibility"] +
scoring.Economic*weights["economic"] +
scoring.Temporal*weights["temporal"] +
scoring.Quality*weights["quality"]
candidate.OverallScore = max(0, min(1, candidate.OverallScore))
return nil
}
// ValidateWithPlugins validates a candidate using plugin validation logic
func (pm *Manager) ValidateWithPlugins(candidate *engine.Candidate) (bool, string) {
plugins := pm.registry.GetByResourceType(candidate.SourceFlow.Type)
if len(plugins) == 0 {
// No plugins available, use default validation
return pm.defaultValidation(candidate)
}
plugin := plugins[0]
// Use plugin validation
valid, reason := plugin.ValidateCandidate(candidate)
if !valid {
return false, fmt.Sprintf("Plugin validation failed: %s", reason)
}
// Also run default validation
valid, defaultReason := pm.defaultValidation(candidate)
if !valid {
return false, fmt.Sprintf("Default validation failed: %s", defaultReason)
}
return true, "All validations passed"
}
// GetEnhancedMatchingCriteria returns enhanced criteria based on plugins
func (pm *Manager) GetEnhancedMatchingCriteria(resourceType domain.ResourceType) *engine.Criteria {
baseCriteria := &engine.Criteria{
ResourceType: resourceType,
MaxDistanceKm: 50.0,
MinCompatibility: 0.5,
MinEconomicScore: 0.3,
MinTemporalScore: 0.4,
MinQualityScore: 0.4,
ExcludeSameOrg: true,
Limit: 50,
}
// Enhance criteria based on resource type
switch resourceType {
case domain.TypeHeat:
baseCriteria.MaxDistanceKm = 30.0 // Heat has shorter transport distance
baseCriteria.MinEconomicScore = 0.4 // Higher economic threshold for heat
case domain.TypeBiowaste:
baseCriteria.MaxDistanceKm = 100.0 // Biowaste can travel farther
baseCriteria.MinQualityScore = 0.6 // Higher quality threshold for biowaste
case domain.TypeWater:
baseCriteria.MaxDistanceKm = 25.0 // Water transport is expensive
baseCriteria.MinCompatibility = 0.6 // Water requires better compatibility
}
return baseCriteria
}
// normalizeEconomicScore converts economic value to a 0-1 score
func (pm *Manager) normalizeEconomicScore(annualSavings float64) float64 {
if annualSavings <= 0 {
return 0
}
// Simple normalization: cap at €100,000 annual savings = score of 1.0
maxSavings := 100000.0
score := min(annualSavings/maxSavings, 1.0)
return score
}
// defaultValidation provides basic validation when no plugins are available
func (pm *Manager) defaultValidation(candidate *engine.Candidate) (bool, string) {
// Basic validation rules
if candidate.OverallScore < 0.3 {
return false, "Overall score below minimum threshold"
}
if candidate.DistanceKm > 100.0 {
return false, "Distance exceeds maximum transport limit"
}
if candidate.EstimatedAnnualSavings < 1000.0 {
return false, "Annual savings below minimum threshold"
}
return true, "Default validation passed"
}
// RegisterDefaultPlugins registers the built-in plugins
func RegisterDefaultPlugins(registry PluginRegistry) error {
// Register heat plugin
heatPlugin := NewHeatPlugin(nil)
if err := registry.Register(heatPlugin); err != nil {
return fmt.Errorf("failed to register heat plugin: %w", err)
}
// Register biowaste plugin
biowastePlugin := NewBiowastePlugin(nil)
if err := registry.Register(biowastePlugin); err != nil {
return fmt.Errorf("failed to register biowaste plugin: %w", err)
}
return nil
}
// Helper functions
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
func max(a, b float64) float64 {
if a > b {
return a
}
return b
}