package service import ( "bugulma/backend/internal/domain" "bugulma/backend/internal/repository" "context" "encoding/json" "log" "time" "github.com/google/uuid" "gorm.io/datatypes" ) // OrganizationService handles business logic for organizations type OrganizationService struct { repo domain.OrganizationRepository graphRepo *repository.GraphOrganizationRepository } // NewOrganizationService creates a new organization service func NewOrganizationService(repo domain.OrganizationRepository, graphRepo *repository.GraphOrganizationRepository) *OrganizationService { return &OrganizationService{ repo: repo, graphRepo: graphRepo, } } // SetGraphRepository sets the graph repository (for optional graph operations) func (s *OrganizationService) SetGraphRepository(graphRepo *repository.GraphOrganizationRepository) { s.graphRepo = graphRepo } // CreateOrganizationRequest represents the request to create an organization type CreateOrganizationRequest struct { // Required fields Name string Subtype domain.OrganizationSubtype Sector domain.OrganizationSector // Basic information Description string LogoURL string GalleryImages []string Website string Address string Latitude float64 Longitude float64 // Business-specific fields LegalForm string PrimaryContactEmail string PrimaryContactPhone string IndustrialSector string CompanySize int YearsOperation int SupplyChainRole domain.SupplyChainRole Certifications []string BusinessFocus []string StrategicVision string DriversBarriers string ReadinessMaturity *int TrustScore *float64 // Technical capabilities TechnicalExpertise []string AvailableTechnology []string ManagementSystems []string // Products and Services SellsProducts []domain.ProductJSON OffersServices []domain.ServiceJSON NeedsServices []domain.ServiceNeedJSON // Cultural/Historical building fields YearBuilt string BuilderOwner string Architect string OriginalPurpose string CurrentUse string Style string Materials string Storeys *int HeritageStatus string // Metadata Verified bool Notes string Sources []string // Relationships TrustNetwork []string ExistingSymbioticRelationships []string } // Create creates a new organization func (s *OrganizationService) Create(ctx context.Context, req CreateOrganizationRequest) (*domain.Organization, error) { // Marshal contact info contactInfo := domain.PrimaryContact{ Email: req.PrimaryContactEmail, Phone: req.PrimaryContactPhone, } contactJSON, _ := json.Marshal(contactInfo) // Marshal arrays certsJSON, _ := json.Marshal(req.Certifications) focusJSON, _ := json.Marshal(req.BusinessFocus) techExpertiseJSON, _ := json.Marshal(req.TechnicalExpertise) technologyJSON, _ := json.Marshal(req.AvailableTechnology) managementJSON, _ := json.Marshal(req.ManagementSystems) trustNetJSON, _ := json.Marshal(req.TrustNetwork) symbiosisJSON, _ := json.Marshal(req.ExistingSymbioticRelationships) galleryImagesJSON, _ := json.Marshal(req.GalleryImages) // Marshal complex JSON objects productsJSON, _ := json.Marshal(req.SellsProducts) servicesJSON, _ := json.Marshal(req.OffersServices) needsJSON, _ := json.Marshal(req.NeedsServices) sourcesJSON, _ := json.Marshal(req.Sources) // Set defaults readinessMaturity := 3 if req.ReadinessMaturity != nil { readinessMaturity = *req.ReadinessMaturity } trustScore := 0.7 if req.TrustScore != nil { trustScore = *req.TrustScore } storeys := 0 if req.Storeys != nil { storeys = *req.Storeys } org := &domain.Organization{ ID: uuid.New().String(), Name: req.Name, Subtype: req.Subtype, Sector: req.Sector, Description: req.Description, LogoURL: req.LogoURL, GalleryImages: datatypes.JSON(galleryImagesJSON), Website: req.Website, Latitude: req.Latitude, Longitude: req.Longitude, LegalForm: req.LegalForm, PrimaryContact: datatypes.JSON(contactJSON), IndustrialSector: req.IndustrialSector, CompanySize: req.CompanySize, YearsOperation: req.YearsOperation, SupplyChainRole: req.SupplyChainRole, Certifications: datatypes.JSON(certsJSON), BusinessFocus: datatypes.JSON(focusJSON), StrategicVision: req.StrategicVision, DriversBarriers: req.DriversBarriers, ReadinessMaturity: readinessMaturity, TrustScore: trustScore, TechnicalExpertise: datatypes.JSON(techExpertiseJSON), AvailableTechnology: datatypes.JSON(technologyJSON), ManagementSystems: datatypes.JSON(managementJSON), SellsProducts: datatypes.JSON(productsJSON), OffersServices: datatypes.JSON(servicesJSON), NeedsServices: datatypes.JSON(needsJSON), YearBuilt: req.YearBuilt, BuilderOwner: req.BuilderOwner, Architect: req.Architect, OriginalPurpose: req.OriginalPurpose, CurrentUse: req.CurrentUse, Style: req.Style, Materials: req.Materials, Storeys: storeys, HeritageStatus: req.HeritageStatus, Verified: req.Verified, Notes: req.Notes, Sources: datatypes.JSON(sourcesJSON), TrustNetwork: datatypes.JSON(trustNetJSON), ExistingSymbioticRelationships: datatypes.JSON(symbiosisJSON), CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.repo.Create(ctx, org); err != nil { return nil, err } if s.graphRepo != nil { if err := s.graphRepo.SyncToGraph(ctx, org); err != nil { log.Printf("Warning: Failed to sync organization %s to graph database: %v", org.ID, err) } } return org, nil } // GetByID retrieves an organization by ID func (s *OrganizationService) GetByID(ctx context.Context, id string) (*domain.Organization, error) { return s.repo.GetByID(ctx, id) } // GetAll retrieves all organizations func (s *OrganizationService) GetAll(ctx context.Context) ([]*domain.Organization, error) { return s.repo.GetAll(ctx) } // GetBySubtype retrieves organizations by subtype func (s *OrganizationService) GetBySubtype(ctx context.Context, subtype domain.OrganizationSubtype) ([]*domain.Organization, error) { return s.repo.GetBySubtype(ctx, subtype) } // GetBySector retrieves organizations by sector func (s *OrganizationService) GetBySector(ctx context.Context, sector domain.OrganizationSector) ([]*domain.Organization, error) { return s.repo.GetBySector(ctx, sector) } // Search performs fuzzy search on organizations // limit: maximum number of results to return (default: 50) func (s *OrganizationService) Search(ctx context.Context, query string, limit int) ([]*domain.Organization, error) { if limit <= 0 { limit = 50 // Default limit } if limit > 200 { limit = 200 // Maximum limit } return s.repo.Search(ctx, query, limit) } // SearchSuggestions returns autocomplete suggestions for search queries // limit: maximum number of suggestions to return (default: 10) func (s *OrganizationService) SearchSuggestions(ctx context.Context, query string, limit int) ([]string, error) { if limit <= 0 { limit = 10 // Default limit } if limit > 50 { limit = 50 // Maximum limit } return s.repo.SearchSuggestions(ctx, query, limit) } // GetByCertification retrieves organizations by certification func (s *OrganizationService) GetByCertification(ctx context.Context, cert string) ([]*domain.Organization, error) { return s.repo.GetByCertification(ctx, cert) } // GetWithinRadius retrieves organizations within a radius func (s *OrganizationService) GetWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.Organization, error) { return s.repo.GetWithinRadius(ctx, lat, lng, radiusKm) } // Update updates an organization func (s *OrganizationService) Update(ctx context.Context, org *domain.Organization) error { org.UpdatedAt = time.Now() if err := s.repo.Update(ctx, org); err != nil { return err } if s.graphRepo != nil { if err := s.graphRepo.SyncToGraph(ctx, org); err != nil { // Log error } } return nil } // Delete deletes an organization func (s *OrganizationService) Delete(ctx context.Context, id string) error { if s.graphRepo != nil { if err := s.graphRepo.DeleteFromGraph(ctx, id); err != nil { // Log error } } return s.repo.Delete(ctx, id) } // AddProduct adds a product to an organization's offerings func (s *OrganizationService) AddProduct(ctx context.Context, orgID string, product domain.ProductJSON) error { org, err := s.repo.GetByID(ctx, orgID) if err != nil { return err } var products []domain.ProductJSON if org.SellsProducts != nil { json.Unmarshal(org.SellsProducts, &products) } products = append(products, product) productsJSON, _ := json.Marshal(products) org.SellsProducts = datatypes.JSON(productsJSON) return s.Update(ctx, org) } // AddService adds a service to an organization's offerings func (s *OrganizationService) AddService(ctx context.Context, orgID string, service domain.ServiceJSON) error { org, err := s.repo.GetByID(ctx, orgID) if err != nil { return err } var services []domain.ServiceJSON if org.OffersServices != nil { json.Unmarshal(org.OffersServices, &services) } services = append(services, service) servicesJSON, _ := json.Marshal(services) org.OffersServices = datatypes.JSON(servicesJSON) return s.Update(ctx, org) } // GetSectorStats returns the top sectors by organization count func (s *OrganizationService) GetSectorStats(ctx context.Context, limit int) ([]domain.SectorStat, error) { return s.repo.GetSectorStats(ctx, limit) } // CalculateSimilarityScores calculates similarity scores for organizations func (s *OrganizationService) CalculateSimilarityScores(ctx context.Context, orgID string, sectorOrgs []*domain.Organization, resourceFlows []*domain.ResourceFlow) ([]*domain.Organization, error) { type orgScore struct { org *domain.Organization score float64 } scores := make(map[string]*orgScore) // Score by sector match (weight: 0.4) for _, similarOrg := range sectorOrgs { if similarOrg.ID == orgID { continue } if scores[similarOrg.ID] == nil { scores[similarOrg.ID] = &orgScore{org: similarOrg, score: 0.0} } scores[similarOrg.ID].score += 0.4 } // Score by resource flow complementarity (weight: 0.6) // Organizations with opposite resource flows (input/output) are more similar for _, flow := range resourceFlows { oppositeDirection := domain.DirectionInput if flow.Direction == domain.DirectionInput { oppositeDirection = domain.DirectionOutput } // Find organizations with opposite flows of the same type complementaryFlows, err := s.repo.GetResourceFlowsByTypeAndDirection(ctx, string(flow.Type), string(oppositeDirection)) if err == nil { for _, compFlow := range complementaryFlows { if compFlow.OrganizationID != orgID { if scores[compFlow.OrganizationID] == nil { // Get the organization compOrg, err := s.repo.GetByID(ctx, compFlow.OrganizationID) if err == nil { scores[compFlow.OrganizationID] = &orgScore{org: compOrg, score: 0.0} } } if scores[compFlow.OrganizationID] != nil { scores[compFlow.OrganizationID].score += 0.6 / float64(len(resourceFlows)) } } } } } // Convert to slice and sort by score var result []*orgScore for _, score := range scores { result = append(result, score) } // Sort by score (descending) for i := 0; i < len(result)-1; i++ { for j := i + 1; j < len(result); j++ { if result[i].score < result[j].score { result[i], result[j] = result[j], result[i] } } } // Extract organizations orgs := make([]*domain.Organization, len(result)) for i, score := range result { orgs[i] = score.org } return orgs, nil } // FindDirectMatches finds direct symbiosis matches for an organization func (s *OrganizationService) FindDirectMatches(ctx context.Context, orgID string, resourceFlows []*domain.ResourceFlow, limit int) ([]DirectSymbiosisMatch, []DirectSymbiosisMatch, error) { var providers []DirectSymbiosisMatch var consumers []DirectSymbiosisMatch // Separate flows by direction var inputFlows []*domain.ResourceFlow // What this org needs (consumes) var outputFlows []*domain.ResourceFlow // What this org provides (produces) for _, flow := range resourceFlows { if flow.Direction == domain.DirectionInput { inputFlows = append(inputFlows, flow) } else if flow.Direction == domain.DirectionOutput { outputFlows = append(outputFlows, flow) } } // Find providers: organizations that can provide what this org needs for _, inputFlow := range inputFlows { // Find organizations that have output flows of the same type matchingOutputs, err := s.repo.GetResourceFlowsByTypeAndDirection(ctx, string(inputFlow.Type), string(domain.DirectionOutput)) if err != nil { continue } for _, outputFlow := range matchingOutputs { // Skip if it's the same organization if outputFlow.OrganizationID == orgID { continue } // Get organization info org, err := s.repo.GetByID(ctx, outputFlow.OrganizationID) if err != nil { continue } providers = append(providers, DirectSymbiosisMatch{ PartnerID: outputFlow.OrganizationID, PartnerName: org.Name, Resource: string(outputFlow.Type), ResourceFlowID: outputFlow.ID, }) } } // Find consumers: organizations that need what this org provides for _, outputFlow := range outputFlows { // Find organizations that have input flows of the same type matchingInputs, err := s.repo.GetResourceFlowsByTypeAndDirection(ctx, string(outputFlow.Type), string(domain.DirectionInput)) if err != nil { continue } for _, inputFlow := range matchingInputs { // Skip if it's the same organization if inputFlow.OrganizationID == orgID { continue } // Get organization info org, err := s.repo.GetByID(ctx, inputFlow.OrganizationID) if err != nil { continue } consumers = append(consumers, DirectSymbiosisMatch{ PartnerID: inputFlow.OrganizationID, PartnerName: org.Name, Resource: string(inputFlow.Type), ResourceFlowID: inputFlow.ID, }) } } // Deduplicate and limit results providers = s.deduplicateMatches(providers, limit) consumers = s.deduplicateMatches(consumers, limit) return providers, consumers, nil } // deduplicateMatches removes duplicate matches and limits the number of results func (s *OrganizationService) deduplicateMatches(matches []DirectSymbiosisMatch, limit int) []DirectSymbiosisMatch { seen := make(map[string]bool) var result []DirectSymbiosisMatch for _, match := range matches { key := match.PartnerID + ":" + match.Resource if !seen[key] && len(result) < limit { seen[key] = true result = append(result, match) } } return result } // DirectSymbiosisMatch represents a direct symbiosis match type DirectSymbiosisMatch struct { PartnerID string `json:"partner_id"` PartnerName string `json:"partner_name"` Resource string `json:"resource"` ResourceFlowID string `json:"resource_flow_id"` } // DirectSymbiosisResponse represents the response for direct symbiosis matches type DirectSymbiosisResponse struct { Providers []DirectSymbiosisMatch `json:"providers"` Consumers []DirectSymbiosisMatch `json:"consumers"` }