turash/bugulma/backend/internal/service/organization_service.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
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)
2025-11-25 06:01:16 +01:00

317 lines
10 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 string
// 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 string) ([]*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)
}