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)
388 lines
13 KiB
Go
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)))
|
|
}
|