mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
500 lines
16 KiB
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"`
|
|
}
|