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

388 lines
13 KiB
Go

package plugins
import (
"encoding/json"
"fmt"
"math"
"strings"
"bugulma/backend/internal/domain"
"bugulma/backend/internal/matching/engine"
)
// BiowastePlugin implements resource-specific matching logic for biowaste resources
type BiowastePlugin struct {
config *BiowastePluginConfig
}
// BiowastePluginConfig contains configuration for the biowaste plugin
type BiowastePluginConfig struct {
// Contamination limits
MaxContaminantPct float64 `json:"max_contaminant_pct"`
ContaminationPenalty float64 `json:"contamination_penalty"`
// Moisture content requirements
MinMoisturePct float64 `json:"min_moisture_pct"`
MaxMoisturePct float64 `json:"max_moisture_pct"`
MoistureMismatchPenalty float64 `json:"moisture_mismatch_penalty"`
// Biodegradability requirements
MinBiodegradabilityPct float64 `json:"min_biodegradability_pct"`
BiodegradabilityBonus float64 `json:"biodegradability_bonus"`
// Regulatory requirements
RequiredCertifications []string `json:"required_certifications"`
CertificationBonus float64 `json:"certification_bonus"`
// Economic factors
WasteDisposalSavings float64 `json:"waste_disposal_savings"` // €/tonne saved by avoiding landfill
TransportCostPerKm float64 `json:"transport_cost_per_km"`
ProcessingCostPerTonne float64 `json:"processing_cost_per_tonne"`
// Distance constraints
MaxTransportDistance float64 `json:"max_transport_distance"`
DistancePenaltyFactor float64 `json:"distance_penalty_factor"`
// Temporal constraints
ShelfLifeDays int `json:"shelf_life_days"`
StorageCostPerDay float64 `json:"storage_cost_per_day"`
}
// DefaultBiowastePluginConfig returns default configuration for biowaste plugin
func DefaultBiowastePluginConfig() *BiowastePluginConfig {
return &BiowastePluginConfig{
MaxContaminantPct: 5.0,
ContaminationPenalty: 0.3,
MinMoisturePct: 40.0,
MaxMoisturePct: 80.0,
MoistureMismatchPenalty: 0.15,
MinBiodegradabilityPct: 70.0,
BiodegradabilityBonus: 0.1,
RequiredCertifications: []string{"ISO 14001", "HACCP"},
CertificationBonus: 0.1,
WasteDisposalSavings: 50.0, // €50/tonne landfill savings
TransportCostPerKm: 0.5, // €0.50/tonne/km
ProcessingCostPerTonne: 20.0, // €20/tonne processing
MaxTransportDistance: 100.0, // 100km max for biowaste
DistancePenaltyFactor: 0.01,
ShelfLifeDays: 7, // Biowaste degrades quickly
StorageCostPerDay: 2.0, // €2/tonne/day storage
}
}
// NewBiowastePlugin creates a new biowaste resource plugin
func NewBiowastePlugin(config *BiowastePluginConfig) *BiowastePlugin {
if config == nil {
config = DefaultBiowastePluginConfig()
}
return &BiowastePlugin{
config: config,
}
}
// Name returns the plugin name
func (bp *BiowastePlugin) Name() string {
return "biowaste_plugin"
}
// ResourceType returns the resource type this plugin handles
func (bp *BiowastePlugin) ResourceType() domain.ResourceType {
return domain.TypeBiowaste
}
// SupportsQualityCheck returns true as biowaste plugin provides quality checks
func (bp *BiowastePlugin) SupportsQualityCheck() bool {
return true
}
// SupportsEconomicCalculation returns true as biowaste plugin provides economic calculations
func (bp *BiowastePlugin) SupportsEconomicCalculation() bool {
return true
}
// SupportsTemporalCheck returns true as biowaste plugin provides temporal checks
func (bp *BiowastePlugin) SupportsTemporalCheck() bool {
return true
}
// CheckQualityCompatibility performs biowaste-specific quality compatibility assessment
func (bp *BiowastePlugin) CheckQualityCompatibility(source, target *domain.ResourceFlow) (float64, error) {
var sourceQuality, targetQuality domain.Quality
if err := json.Unmarshal(source.Quality, &sourceQuality); err != nil {
return 0, fmt.Errorf("failed to unmarshal source quality: %w", err)
}
if err := json.Unmarshal(target.Quality, &targetQuality); err != nil {
return 0, fmt.Errorf("failed to unmarshal target quality: %w", err)
}
score := 1.0
var issues []string
// Contamination check (critical for biowaste)
if sourceQuality.PurityPct != nil {
contaminationPct := 100.0 - *sourceQuality.PurityPct // Contamination = 100 - purity
if contaminationPct > bp.config.MaxContaminantPct {
return 0, fmt.Errorf("contamination level %.1f%% exceeds maximum %.1f%%",
contaminationPct, bp.config.MaxContaminantPct)
}
score -= (contaminationPct / 100.0) * bp.config.ContaminationPenalty
if contaminationPct > bp.config.MaxContaminantPct/2 {
issues = append(issues, fmt.Sprintf("High contamination: %.1f%%", contaminationPct))
}
}
// Moisture content compatibility
if sourceQuality.MoisturePct != nil {
moisture := *sourceQuality.MoisturePct
if moisture < bp.config.MinMoisturePct {
score -= bp.config.MoistureMismatchPenalty
issues = append(issues, fmt.Sprintf("Low moisture: %.1f%% (min %.1f%%)", moisture, bp.config.MinMoisturePct))
} else if moisture > bp.config.MaxMoisturePct {
score -= bp.config.MoistureMismatchPenalty
issues = append(issues, fmt.Sprintf("High moisture: %.1f%% (max %.1f%%)", moisture, bp.config.MaxMoisturePct))
}
}
// Biodegradability check
if sourceQuality.Composition != "" {
// Simple check for biodegradability keywords
composition := strings.ToLower(sourceQuality.Composition)
biodegradable := strings.Contains(composition, "organic") ||
strings.Contains(composition, "biodegradable") ||
strings.Contains(composition, "food waste") ||
strings.Contains(composition, "green waste")
if biodegradable {
score += bp.config.BiodegradabilityBonus
} else {
score -= 0.2 // Penalty for non-biodegradable waste
issues = append(issues, "Composition may not be fully biodegradable")
}
}
// Certification compatibility
if len(sourceQuality.Certifications) > 0 {
matchingCerts := 0
for _, required := range bp.config.RequiredCertifications {
for _, has := range sourceQuality.Certifications {
if strings.Contains(has, required) {
matchingCerts++
break
}
}
}
if matchingCerts > 0 {
score += bp.config.CertificationBonus
}
}
// Particle size consideration for processing
if sourceQuality.ParticleSizeMicron != nil {
size := *sourceQuality.ParticleSizeMicron
if size > 50000 { // Very large particles
score -= 0.05
issues = append(issues, "Large particle size may require additional processing")
}
}
if len(issues) > 0 && score < 0.7 {
return score, fmt.Errorf("quality issues: %s", strings.Join(issues, "; "))
}
return math.Max(0, math.Min(1, score)), nil
}
// CalculateEconomicImpact performs biowaste-specific economic impact calculation
func (bp *BiowastePlugin) CalculateEconomicImpact(source, target *domain.ResourceFlow, distance float64) (float64, error) {
var sourceEcon, targetEcon domain.EconomicData
var sourceQty domain.Quantity
if err := json.Unmarshal(source.EconomicData, &sourceEcon); err != nil {
return 0, fmt.Errorf("failed to unmarshal source economic data: %w", err)
}
if err := json.Unmarshal(target.EconomicData, &targetEcon); err != nil {
return 0, fmt.Errorf("failed to unmarshal target economic data: %w", err)
}
if err := json.Unmarshal(source.Quantity, &sourceQty); err != nil {
return 0, fmt.Errorf("failed to unmarshal source quantity: %w", err)
}
// Base economic calculation for biowaste
// Savings = waste disposal cost avoided + value from reuse - transport - processing
// Waste disposal savings (avoiding landfill/incineration)
disposalSavings := bp.config.WasteDisposalSavings * sourceQty.Amount
// Transport costs
transportCost := distance * bp.config.TransportCostPerKm * sourceQty.Amount
// Processing costs for the receiving facility
processingCost := bp.config.ProcessingCostPerTonne * sourceQty.Amount
// Value from reuse (what the target pays for the waste as input)
reuseValue := targetEcon.CostIn * sourceQty.Amount
// Storage costs due to short shelf life
storageDays := float64(bp.config.ShelfLifeDays)
storageCost := bp.config.StorageCostPerDay * sourceQty.Amount * storageDays
// Distance penalty for biowaste degradation
distancePenalty := distance * bp.config.DistancePenaltyFactor * sourceQty.Amount
// Annual volume calculation
annualVolume := sourceQty.Amount
switch sourceQty.TemporalUnit {
case domain.UnitPerDay:
annualVolume *= 365
case domain.UnitPerWeek:
annualVolume *= 52
case domain.UnitPerMonth:
annualVolume *= 12
}
// Total annual economic impact
totalAnnualSavings := (disposalSavings + reuseValue - transportCost - processingCost - storageCost - distancePenalty) * annualVolume
// Distance constraint check
if distance > bp.config.MaxTransportDistance {
return totalAnnualSavings, fmt.Errorf("transport distance %.1fkm exceeds maximum %.1fkm for biowaste",
distance, bp.config.MaxTransportDistance)
}
// Minimum viability check
if totalAnnualSavings < 1000.0 { // €1,000 minimum annual savings
return totalAnnualSavings, fmt.Errorf("insufficient economic viability: annual savings €%.0f below minimum €1,000",
totalAnnualSavings)
}
return totalAnnualSavings, nil
}
// CheckTemporalCompatibility performs biowaste-specific temporal compatibility assessment
func (bp *BiowastePlugin) CheckTemporalCompatibility(source, target *domain.ResourceFlow) (float64, error) {
var sourceTime, targetTime domain.TimeProfile
if err := json.Unmarshal(source.TimeProfile, &sourceTime); err != nil {
return 0, fmt.Errorf("failed to unmarshal source time profile: %w", err)
}
if err := json.Unmarshal(target.TimeProfile, &targetTime); err != nil {
return 0, fmt.Errorf("failed to unmarshal target time profile: %w", err)
}
score := 1.0
// Biowaste has short shelf life - temporal alignment is critical
sourceHours := bp.calculateWeeklyHours(sourceTime.Availability)
targetHours := bp.calculateWeeklyHours(targetTime.Availability)
overlapHours := math.Min(sourceHours, targetHours)
// Biowaste requires more frequent collection/processing
minOverlapHours := 40.0 // 40 hours/week minimum for biowaste
if overlapHours < minOverlapHours {
return 0, fmt.Errorf("insufficient temporal overlap: %.0f hours/week < minimum %.0f hours/week for biowaste",
overlapHours, minOverlapHours)
}
overlapScore := overlapHours / 168.0 // 168 hours in a week
score *= overlapScore
// Seasonality consideration (food waste peaks in certain seasons)
if len(sourceTime.Seasonality) > 0 && len(targetTime.Seasonality) > 0 {
seasonOverlap := bp.calculateSeasonalOverlap(sourceTime.Seasonality, targetTime.Seasonality)
score *= (0.8 + 0.2*seasonOverlap) // 80-100% based on seasonal alignment
}
// Predictability is crucial for biowaste logistics
sourcePredictability := 0.5
targetPredictability := 0.5
if sourceTime.PredictabilityScore != nil {
sourcePredictability = *sourceTime.PredictabilityScore
}
if targetTime.PredictabilityScore != nil {
targetPredictability = *targetTime.PredictabilityScore
}
// Biowaste requires high predictability
minPredictability := 0.7
avgPredictability := (sourcePredictability + targetPredictability) / 2
if avgPredictability < minPredictability {
score *= 0.5 // Significant penalty for unpredictable supply
} else {
score *= avgPredictability
}
// Lead time consideration
requiredLeadTime := 24.0 // 24 hours for biowaste
if sourceTime.LeadTimeDays != nil && targetTime.LeadTimeDays != nil {
actualLeadTime := math.Max(float64(*sourceTime.LeadTimeDays), float64(*targetTime.LeadTimeDays)) * 24
if actualLeadTime > requiredLeadTime {
score *= 0.9 // Minor penalty for longer lead times
}
}
return math.Max(0, math.Min(1, score)), nil
}
// ValidateCandidate performs biowaste-specific candidate validation
func (bp *BiowastePlugin) ValidateCandidate(candidate *engine.Candidate) (bool, string) {
// Biowaste-specific validation rules
// 1. Distance constraint
if candidate.DistanceKm > bp.config.MaxTransportDistance {
return false, fmt.Sprintf("Transport distance %.1fkm exceeds maximum %.1fkm for biowaste",
candidate.DistanceKm, bp.config.MaxTransportDistance)
}
// 2. Minimum annual savings
if candidate.EstimatedAnnualSavings < 2000.0 { // Higher threshold for biowaste
return false, "Annual savings below €2,000 threshold for biowaste matching"
}
// 3. Quality score threshold (higher for biowaste due to contamination risks)
if candidate.QualityScore < 0.7 {
return false, "Quality score below 0.7 threshold for biowaste matching"
}
// 4. Temporal score (critical for perishable waste)
if candidate.TemporalScore < 0.6 {
return false, "Temporal compatibility below 0.6 threshold for biowaste matching"
}
return true, "Biowaste matching candidate validation passed"
}
// calculateWeeklyHours calculates total available hours per week from availability map
func (bp *BiowastePlugin) calculateWeeklyHours(availability map[string]domain.TimeRange) float64 {
totalHours := 0.0
for _, timeRange := range availability {
// Simple calculation - in production this would parse time ranges properly
hours := 8.0 // Default 8 hours per day
totalHours += hours
_ = timeRange // Mark as used to avoid linter warning
}
return totalHours
}
// calculateSeasonalOverlap calculates overlap between seasonality arrays
func (bp *BiowastePlugin) calculateSeasonalOverlap(source, target []string) float64 {
if len(source) == 0 || len(target) == 0 {
return 0
}
overlap := 0
for _, s := range source {
for _, t := range target {
if s == t {
overlap++
break
}
}
}
return float64(overlap) / math.Max(float64(len(source)), float64(len(target)))
}