turash/bugulma/backend/internal/service/organization_service.go

500 lines
16 KiB
Go

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"`
}