mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
325 lines
11 KiB
Go
325 lines
11 KiB
Go
package service_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/geospatial"
|
|
"bugulma/backend/internal/repository"
|
|
"bugulma/backend/internal/service"
|
|
"bugulma/backend/internal/testutils"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/datatypes"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type FacilityLocationOptimizerTestSuite struct {
|
|
suite.Suite
|
|
db *gorm.DB
|
|
siteRepo domain.SiteRepository
|
|
geoRepo domain.GeographicalFeatureRepository
|
|
geoSvc *service.GeospatialService
|
|
spatialMatcher *service.SpatialResourceMatcher
|
|
envSvc *service.EnvironmentalImpactService
|
|
transportSvc *service.TransportationService
|
|
geoCalc geospatial.Calculator
|
|
optimizer *service.FacilityLocationOptimizer
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) SetupTest() {
|
|
suite.db = testutils.SetupTestDB(suite.T())
|
|
suite.siteRepo = repository.NewSiteRepository(suite.db)
|
|
suite.geoRepo = repository.NewGeographicalFeatureRepository(suite.db)
|
|
suite.geoSvc = service.NewGeospatialService(suite.db, suite.geoRepo)
|
|
suite.geoCalc = geospatial.NewCalculatorWithDefaults()
|
|
suite.transportSvc = service.NewTransportationService(suite.geoCalc)
|
|
suite.spatialMatcher = service.NewSpatialResourceMatcher(
|
|
suite.geoRepo,
|
|
suite.siteRepo,
|
|
nil, // resourceFlowRepo - not needed for basic tests
|
|
suite.geoSvc,
|
|
suite.transportSvc,
|
|
suite.geoCalc,
|
|
)
|
|
suite.envSvc = service.NewEnvironmentalImpactService(suite.geoRepo, suite.siteRepo, suite.geoSvc, suite.geoCalc)
|
|
suite.optimizer = service.NewFacilityLocationOptimizer(
|
|
suite.geoRepo,
|
|
suite.siteRepo,
|
|
suite.geoSvc,
|
|
suite.spatialMatcher,
|
|
suite.envSvc,
|
|
suite.transportSvc,
|
|
)
|
|
|
|
// Create test organization
|
|
org := &domain.Organization{ID: "org-facility-test", Name: "Facility Test Organization"}
|
|
err := repository.NewOrganizationRepository(suite.db).Create(context.Background(), org)
|
|
suite.Require().NoError(err)
|
|
}
|
|
|
|
func TestFacilityLocationOptimizer(t *testing.T) {
|
|
suite.Run(t, new(FacilityLocationOptimizerTestSuite))
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestNewFacilityLocationOptimizer() {
|
|
assert.NotNil(suite.T(), suite.optimizer)
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) setupTestSites() []*domain.Site {
|
|
sites := []*domain.Site{
|
|
{
|
|
ID: "site-industrial-good",
|
|
Name: "Good Industrial Site",
|
|
Latitude: 52.5200,
|
|
Longitude: 13.4050,
|
|
SiteType: domain.SiteTypeIndustrial,
|
|
OwnerOrganizationID: "org-facility-test",
|
|
EnvironmentalImpact: "low_impact",
|
|
AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "rail_access"]`),
|
|
ParkingSpaces: 50,
|
|
LoadingDocks: 5,
|
|
FloorAreaM2: 5000.0,
|
|
},
|
|
{
|
|
ID: "site-office-poor",
|
|
Name: "Poor Office Site",
|
|
Latitude: 52.5300,
|
|
Longitude: 13.4150,
|
|
SiteType: domain.SiteTypeOffice,
|
|
OwnerOrganizationID: "org-facility-test",
|
|
EnvironmentalImpact: "high_impact",
|
|
AvailableUtilities: datatypes.JSON(`["electricity"]`),
|
|
ParkingSpaces: 10,
|
|
LoadingDocks: 1,
|
|
FloorAreaM2: 1000.0,
|
|
},
|
|
{
|
|
ID: "site-commercial-excellent",
|
|
Name: "Excellent Commercial Site",
|
|
Latitude: 52.5400,
|
|
Longitude: 13.4250,
|
|
SiteType: domain.SiteTypeRetail,
|
|
OwnerOrganizationID: "org-facility-test",
|
|
EnvironmentalImpact: "eco_friendly",
|
|
AvailableUtilities: datatypes.JSON(`["electricity", "gas", "water", "heating", "cooling", "pipeline_access"]`),
|
|
ParkingSpaces: 100,
|
|
LoadingDocks: 10,
|
|
FloorAreaM2: 10000.0,
|
|
},
|
|
}
|
|
|
|
for _, site := range sites {
|
|
err := suite.siteRepo.Create(context.Background(), site)
|
|
suite.Require().NoError(err)
|
|
}
|
|
|
|
return sites
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_BasicOptimization() {
|
|
suite.setupTestSites()
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 50.0,
|
|
MaxTransportCost: 200.0,
|
|
MinEnvironmentalScore: 0.0,
|
|
RequiredUtilities: []string{"electricity"},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 5,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
assert.NotEmpty(suite.T(), results)
|
|
assert.LessOrEqual(suite.T(), len(results), criteria.MaxResults)
|
|
|
|
// Verify results are sorted by score (highest first)
|
|
for i := 0; i < len(results)-1; i++ {
|
|
assert.GreaterOrEqual(suite.T(), results[i].Score.OverallScore, results[i+1].Score.OverallScore)
|
|
}
|
|
|
|
// Verify all results meet minimum criteria
|
|
for _, result := range results {
|
|
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0) // Max possible score
|
|
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, criteria.MinEnvironmentalScore)
|
|
}
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_WithUtilityRequirements() {
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 100.0,
|
|
MaxTransportCost: 500.0,
|
|
MinEnvironmentalScore: 0.0,
|
|
RequiredUtilities: []string{"gas", "water", "rail_access"}, // Very specific requirements
|
|
MinFloorAreaM2: 1500.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
assert.NotEmpty(suite.T(), results)
|
|
|
|
// Check that results have reasonable scores
|
|
for _, result := range results {
|
|
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
|
|
}
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EnvironmentalFiltering() {
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 100.0,
|
|
MaxTransportCost: 500.0,
|
|
MinEnvironmentalScore: 7.0, // High environmental requirement
|
|
RequiredUtilities: []string{"electricity"},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
|
|
// Only sites meeting environmental criteria should be returned
|
|
for _, result := range results {
|
|
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 7.0, "All results should meet environmental criteria")
|
|
}
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_SiteTypeFiltering() {
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 100.0,
|
|
MaxTransportCost: 500.0,
|
|
MinEnvironmentalScore: 0.0,
|
|
RequiredUtilities: []string{"electricity"},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
|
|
// Check that results have reasonable scores
|
|
for _, result := range results {
|
|
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
|
|
}
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_NoMatchingSites() {
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 100.0,
|
|
MaxTransportCost: 500.0,
|
|
MinEnvironmentalScore: 9.5, // Very high requirement
|
|
RequiredUtilities: []string{"nonexistent_utility"}, // Impossible requirement
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
assert.Empty(suite.T(), results, "No sites should match impossible criteria")
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_EmptyCriteria() {
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{},
|
|
ResourceRadiusKm: 0.0,
|
|
MaxTransportCost: 0.0,
|
|
MinEnvironmentalScore: 0.0,
|
|
RequiredUtilities: []string{},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights, // No limit
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
|
|
// Check that we get some results
|
|
assert.Greater(suite.T(), len(results), 0, "Should return some results")
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_MaxResults() {
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{},
|
|
ResourceRadiusKm: 100.0,
|
|
MaxTransportCost: 1000.0,
|
|
MinEnvironmentalScore: 0.0,
|
|
RequiredUtilities: []string{},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 2, // Limit to 2 results
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
assert.Len(suite.T(), results, 2, "Should return exactly MaxResults items")
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_ScoreCalculation() {
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 50.0,
|
|
MaxTransportCost: 200.0,
|
|
MinEnvironmentalScore: 3.0,
|
|
RequiredUtilities: []string{"electricity"},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
|
|
for _, result := range results {
|
|
// Verify score components are present
|
|
assert.GreaterOrEqual(suite.T(), result.Score.TransportationScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.TransportationScore, 10.0)
|
|
assert.GreaterOrEqual(suite.T(), result.Score.EnvironmentalScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.EnvironmentalScore, 10.0)
|
|
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
|
|
assert.LessOrEqual(suite.T(), result.Score.OverallScore, 20.0)
|
|
}
|
|
}
|
|
|
|
func (suite *FacilityLocationOptimizerTestSuite) TestOptimizeFacilityLocation_Explanations() {
|
|
|
|
criteria := service.LocationCriteria{
|
|
RequiredResources: []domain.ResourceType{domain.TypeHeat},
|
|
ResourceRadiusKm: 50.0,
|
|
MaxTransportCost: 200.0,
|
|
MinEnvironmentalScore: 5.0,
|
|
RequiredUtilities: []string{"electricity", "gas"},
|
|
MinFloorAreaM2: 1000.0,
|
|
MaxResults: 10,
|
|
Weights: service.DefaultWeights,
|
|
}
|
|
|
|
results, err := suite.optimizer.FindOptimalLocations(context.Background(), criteria)
|
|
assert.NoError(suite.T(), err)
|
|
|
|
// Check that results have valid scores
|
|
for _, result := range results {
|
|
assert.NotNil(suite.T(), result.Score, "Result should have a score")
|
|
assert.GreaterOrEqual(suite.T(), result.Score.OverallScore, 0.0)
|
|
}
|
|
}
|