mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
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)
739 lines
22 KiB
Go
739 lines
22 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/matching"
|
|
"bugulma/backend/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
type OrganizationHandler struct {
|
|
orgService *service.OrganizationService
|
|
imageService *service.ImageService
|
|
resourceFlowService *service.ResourceFlowService
|
|
matchingService *matching.Service
|
|
}
|
|
|
|
func NewOrganizationHandler(orgService *service.OrganizationService, imageService *service.ImageService, resourceFlowService *service.ResourceFlowService, matchingService *matching.Service) *OrganizationHandler {
|
|
return &OrganizationHandler{
|
|
orgService: orgService,
|
|
imageService: imageService,
|
|
resourceFlowService: resourceFlowService,
|
|
matchingService: matchingService,
|
|
}
|
|
}
|
|
|
|
type CreateOrganizationRequest struct {
|
|
// Required fields
|
|
Name string `json:"name" binding:"required"`
|
|
Sector string `json:"sector" binding:"required"`
|
|
Subtype string `json:"subtype" binding:"required"`
|
|
|
|
// Basic information
|
|
Description string `json:"description"`
|
|
LogoURL string `json:"logoUrl"`
|
|
GalleryImages []string `json:"galleryImages"`
|
|
Website string `json:"website"`
|
|
Address string `json:"address"`
|
|
Latitude float64 `json:"latitude"`
|
|
Longitude float64 `json:"longitude"`
|
|
|
|
// Business-specific fields
|
|
LegalForm string `json:"legalForm"`
|
|
PrimaryContactEmail string `json:"primaryContactEmail"`
|
|
PrimaryContactPhone string `json:"primaryContactPhone"`
|
|
IndustrialSector string `json:"industrialSector"`
|
|
CompanySize int `json:"companySize"`
|
|
YearsOperation int `json:"yearsOperation"`
|
|
SupplyChainRole string `json:"supplyChainRole"`
|
|
Certifications []string `json:"certifications"`
|
|
BusinessFocus []string `json:"businessFocus"`
|
|
StrategicVision string `json:"strategicVision"`
|
|
DriversBarriers string `json:"driversBarriers"`
|
|
ReadinessMaturity *int `json:"readinessMaturity"` // 1-5
|
|
TrustScore *float64 `json:"trustScore"` // 0.0-1.0
|
|
|
|
// Technical capabilities
|
|
TechnicalExpertise []string `json:"technicalExpertise"`
|
|
AvailableTechnology []string `json:"availableTechnology"`
|
|
ManagementSystems []string `json:"managementSystems"`
|
|
|
|
// Products and Services
|
|
SellsProducts []domain.ProductJSON `json:"sellsProducts"`
|
|
OffersServices []domain.ServiceJSON `json:"offersServices"`
|
|
NeedsServices []domain.ServiceNeedJSON `json:"needsServices"`
|
|
|
|
// Cultural/Historical building fields
|
|
YearBuilt string `json:"yearBuilt"`
|
|
BuilderOwner string `json:"builderOwner"`
|
|
Architect string `json:"architect"`
|
|
OriginalPurpose string `json:"originalPurpose"`
|
|
CurrentUse string `json:"currentUse"`
|
|
Style string `json:"style"`
|
|
Materials string `json:"materials"`
|
|
Storeys *int `json:"storeys"`
|
|
HeritageStatus string `json:"heritageStatus"`
|
|
|
|
// Metadata
|
|
Verified bool `json:"verified"`
|
|
Notes string `json:"notes"`
|
|
Sources []string `json:"sources"`
|
|
|
|
// Relationships
|
|
TrustNetwork []string `json:"trustNetwork"`
|
|
ExistingSymbioticRelationships []string `json:"existingSymbioticRelationships"`
|
|
}
|
|
|
|
func (h *OrganizationHandler) Create(c *gin.Context) {
|
|
var req CreateOrganizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
orgReq := service.CreateOrganizationRequest{
|
|
Name: req.Name,
|
|
Subtype: domain.OrganizationSubtype(req.Subtype),
|
|
Sector: req.Sector,
|
|
Description: req.Description,
|
|
LogoURL: req.LogoURL,
|
|
GalleryImages: req.GalleryImages,
|
|
Website: req.Website,
|
|
Address: req.Address,
|
|
Latitude: req.Latitude,
|
|
Longitude: req.Longitude,
|
|
LegalForm: req.LegalForm,
|
|
PrimaryContactEmail: req.PrimaryContactEmail,
|
|
PrimaryContactPhone: req.PrimaryContactPhone,
|
|
IndustrialSector: req.IndustrialSector,
|
|
CompanySize: req.CompanySize,
|
|
YearsOperation: req.YearsOperation,
|
|
SupplyChainRole: domain.SupplyChainRole(req.SupplyChainRole),
|
|
Certifications: req.Certifications,
|
|
BusinessFocus: req.BusinessFocus,
|
|
StrategicVision: req.StrategicVision,
|
|
DriversBarriers: req.DriversBarriers,
|
|
ReadinessMaturity: req.ReadinessMaturity,
|
|
TrustScore: req.TrustScore,
|
|
TechnicalExpertise: req.TechnicalExpertise,
|
|
AvailableTechnology: req.AvailableTechnology,
|
|
ManagementSystems: req.ManagementSystems,
|
|
SellsProducts: req.SellsProducts,
|
|
OffersServices: req.OffersServices,
|
|
NeedsServices: req.NeedsServices,
|
|
YearBuilt: req.YearBuilt,
|
|
BuilderOwner: req.BuilderOwner,
|
|
Architect: req.Architect,
|
|
OriginalPurpose: req.OriginalPurpose,
|
|
CurrentUse: req.CurrentUse,
|
|
Style: req.Style,
|
|
Materials: req.Materials,
|
|
Storeys: req.Storeys,
|
|
HeritageStatus: req.HeritageStatus,
|
|
Verified: req.Verified,
|
|
Notes: req.Notes,
|
|
Sources: req.Sources,
|
|
TrustNetwork: req.TrustNetwork,
|
|
ExistingSymbioticRelationships: req.ExistingSymbioticRelationships,
|
|
}
|
|
|
|
org, err := h.orgService.Create(c.Request.Context(), orgReq)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, org)
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetByID(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
org, err := h.orgService.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, org)
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetAll(c *gin.Context) {
|
|
// Check for sector filter
|
|
sector := c.Query("sector")
|
|
if sector != "" {
|
|
orgs, err := h.orgService.GetBySector(c.Request.Context(), sector)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, orgs)
|
|
return
|
|
}
|
|
|
|
// No filter - return all organizations
|
|
orgs, err := h.orgService.GetAll(c.Request.Context())
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orgs)
|
|
}
|
|
|
|
func (h *OrganizationHandler) Update(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var req CreateOrganizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
org, err := h.orgService.GetByID(c.Request.Context(), id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
// Update fields
|
|
org.Name = req.Name
|
|
org.Sector = req.Sector
|
|
org.Subtype = domain.OrganizationSubtype(req.Subtype)
|
|
org.Description = req.Description
|
|
org.LogoURL = req.LogoURL
|
|
org.Website = req.Website
|
|
|
|
if err := h.orgService.Update(c.Request.Context(), org); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, org)
|
|
}
|
|
|
|
func (h *OrganizationHandler) Delete(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
if err := h.orgService.Delete(c.Request.Context(), id); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetBySubtype(c *gin.Context) {
|
|
subtype := c.Param("subtype")
|
|
|
|
orgs, err := h.orgService.GetBySubtype(c.Request.Context(), domain.OrganizationSubtype(subtype))
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orgs)
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetBySector(c *gin.Context) {
|
|
sector := c.Param("sector")
|
|
|
|
orgs, err := h.orgService.GetBySector(c.Request.Context(), sector)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orgs)
|
|
}
|
|
|
|
// GetSectorStats returns sector statistics (top sectors by organization count)
|
|
func (h *OrganizationHandler) GetSectorStats(c *gin.Context) { // force reload
|
|
// Get limit from query param, default to 10
|
|
limitStr := c.DefaultQuery("limit", "10")
|
|
limit, err := strconv.Atoi(limitStr)
|
|
if err != nil || limit < 1 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
|
|
stats, err := h.orgService.GetSectorStats(c.Request.Context(), limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"sectors": stats})
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetByCertification(c *gin.Context) {
|
|
cert := c.Param("cert")
|
|
|
|
orgs, err := h.orgService.GetByCertification(c.Request.Context(), cert)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orgs)
|
|
}
|
|
|
|
// Search handles search requests for organizations
|
|
// Query parameters:
|
|
// - q: search query string (required)
|
|
// - limit: maximum number of results (optional, default: 50, max: 200)
|
|
func (h *OrganizationHandler) Search(c *gin.Context) {
|
|
query := c.Query("q")
|
|
if query == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter 'q' is required"})
|
|
return
|
|
}
|
|
|
|
limit := 50 // Default limit
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
|
limit = parsedLimit
|
|
if limit > 200 {
|
|
limit = 200 // Maximum limit
|
|
}
|
|
}
|
|
}
|
|
|
|
orgs, err := h.orgService.Search(c.Request.Context(), query, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Return structured response matching frontend schema
|
|
response := gin.H{
|
|
"organizations": orgs,
|
|
"count": len(orgs),
|
|
"total": len(orgs), // For now, we don't have total count from search
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// SearchSuggestions handles autocomplete/suggestion requests
|
|
// Query parameters:
|
|
// - q: search query string (required)
|
|
// - limit: maximum number of suggestions (optional, default: 10, max: 50)
|
|
func (h *OrganizationHandler) SearchSuggestions(c *gin.Context) {
|
|
query := c.Query("q")
|
|
if query == "" {
|
|
c.JSON(http.StatusOK, []string{}) // Return empty array for empty query
|
|
return
|
|
}
|
|
|
|
limit := 10 // Default limit
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
|
limit = parsedLimit
|
|
if limit > 50 {
|
|
limit = 50 // Maximum limit
|
|
}
|
|
}
|
|
}
|
|
|
|
suggestions, err := h.orgService.SearchSuggestions(c.Request.Context(), query, limit)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, suggestions)
|
|
}
|
|
|
|
func (h *OrganizationHandler) GetNearby(c *gin.Context) {
|
|
var query struct {
|
|
Latitude float64 `form:"lat" binding:"required"`
|
|
Longitude float64 `form:"lng" binding:"required"`
|
|
RadiusKm float64 `form:"radius" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindQuery(&query); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
orgs, err := h.orgService.GetWithinRadius(c.Request.Context(), query.Latitude, query.Longitude, query.RadiusKm)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, orgs)
|
|
}
|
|
|
|
// UploadLogo handles logo image uploads for organizations
|
|
func (h *OrganizationHandler) UploadLogo(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
|
|
// Get the uploaded file
|
|
file, header, err := c.Request.FormFile("logo")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No logo file provided"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Save the image
|
|
uploadedImage, err := h.imageService.SaveImage(file, header, "logos")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save logo: %v", err)})
|
|
return
|
|
}
|
|
|
|
// Update the organization with the new logo URL
|
|
org, err := h.orgService.GetByID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
// Delete old logo if it exists
|
|
if org.LogoURL != "" {
|
|
h.imageService.DeleteImage(org.LogoURL)
|
|
}
|
|
|
|
org.LogoURL = uploadedImage.URL
|
|
if err := h.orgService.Update(c.Request.Context(), org); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": uploadedImage.URL,
|
|
"message": "Logo uploaded successfully",
|
|
})
|
|
}
|
|
|
|
// UploadGalleryImage handles gallery image uploads for organizations
|
|
func (h *OrganizationHandler) UploadGalleryImage(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
|
|
// Get the uploaded file
|
|
file, header, err := c.Request.FormFile("image")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "No image file provided"})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Save the image
|
|
uploadedImage, err := h.imageService.SaveImage(file, header, "gallery")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save image: %v", err)})
|
|
return
|
|
}
|
|
|
|
// Update the organization by adding the image to gallery
|
|
org, err := h.orgService.GetByID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
// Get current gallery images
|
|
galleryImages := []string{}
|
|
if len(org.GalleryImages) > 0 {
|
|
// Parse the JSON field
|
|
var currentImages []string
|
|
if err := json.Unmarshal(org.GalleryImages, ¤tImages); err == nil {
|
|
galleryImages = currentImages
|
|
}
|
|
}
|
|
|
|
// Add new image
|
|
galleryImages = append(galleryImages, uploadedImage.URL)
|
|
|
|
// Convert to JSON for storage
|
|
galleryJSON, _ := json.Marshal(galleryImages)
|
|
org.GalleryImages = datatypes.JSON(galleryJSON)
|
|
|
|
if err := h.orgService.Update(c.Request.Context(), org); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"url": uploadedImage.URL,
|
|
"galleryImages": galleryImages,
|
|
"message": "Gallery image uploaded successfully",
|
|
})
|
|
}
|
|
|
|
// DeleteGalleryImage removes a gallery image from an organization
|
|
func (h *OrganizationHandler) DeleteGalleryImage(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
imageURL := c.Query("url")
|
|
|
|
if imageURL == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Image URL required"})
|
|
return
|
|
}
|
|
|
|
// Get the organization
|
|
org, err := h.orgService.GetByID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
// Remove the image from gallery
|
|
galleryImages := []string{}
|
|
if len(org.GalleryImages) > 0 {
|
|
// Parse the JSON field
|
|
var currentImages []string
|
|
if err := json.Unmarshal(org.GalleryImages, ¤tImages); err == nil {
|
|
for _, img := range currentImages {
|
|
if img != imageURL {
|
|
galleryImages = append(galleryImages, img)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delete the file from disk
|
|
if err := h.imageService.DeleteImage(imageURL); err != nil {
|
|
// Log error but continue with database update
|
|
fmt.Printf("Warning: failed to delete image file: %v\n", err)
|
|
}
|
|
|
|
// Convert to JSON for storage
|
|
galleryJSON, _ := json.Marshal(galleryImages)
|
|
org.GalleryImages = datatypes.JSON(galleryJSON)
|
|
if err := h.orgService.Update(c.Request.Context(), org); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update organization"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"galleryImages": galleryImages,
|
|
"message": "Gallery image deleted successfully",
|
|
})
|
|
}
|
|
|
|
// GetSimilarOrganizations returns organizations similar to the given organization
|
|
func (h *OrganizationHandler) GetSimilarOrganizations(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
limitStr := c.DefaultQuery("limit", "5")
|
|
|
|
limit := 5
|
|
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
|
limit = parsedLimit
|
|
}
|
|
|
|
// For now, return organizations of the same sector/type
|
|
// TODO: Implement more sophisticated similarity algorithm
|
|
org, err := h.orgService.GetByID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Organization not found"})
|
|
return
|
|
}
|
|
|
|
// Get organizations by sector
|
|
similarOrgs, err := h.orgService.GetBySector(c.Request.Context(), org.Sector)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get similar organizations"})
|
|
return
|
|
}
|
|
|
|
// Filter out the original organization and limit results
|
|
var result []*domain.Organization
|
|
for _, similarOrg := range similarOrgs {
|
|
if similarOrg.ID != orgID && len(result) < limit {
|
|
result = append(result, similarOrg)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// GetOrganizationProposals returns proposals related to the organization
|
|
func (h *OrganizationHandler) GetOrganizationProposals(c *gin.Context) {
|
|
// orgID := c.Param("id") // TODO: Use when implementing proposals functionality
|
|
|
|
// TODO: Implement proposals functionality
|
|
// For now, return empty array
|
|
c.JSON(http.StatusOK, []interface{}{})
|
|
}
|
|
|
|
// GetOrganizationResources returns resource flows for the organization
|
|
func (h *OrganizationHandler) GetOrganizationResources(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
|
|
// Get resource flows by organization ID
|
|
resourceFlows, err := h.resourceFlowService.GetByOrganizationID(c.Request.Context(), orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get organization resources"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, resourceFlows)
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// GetDirectMatches returns direct matches for the organization
|
|
func (h *OrganizationHandler) GetDirectMatches(c *gin.Context) {
|
|
orgID := c.Param("id")
|
|
if orgID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Organization ID is required"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Get organization's resource flows
|
|
resourceFlows, err := h.resourceFlowService.GetByOrganizationID(ctx, orgID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get resource flows: %v", err)})
|
|
return
|
|
}
|
|
|
|
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 := h.resourceFlowService.GetByTypeAndDirection(ctx, inputFlow.Type, 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 := h.orgService.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 := h.resourceFlowService.GetByTypeAndDirection(ctx, outputFlow.Type, 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 := h.orgService.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,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Remove duplicates and limit results
|
|
providers = h.deduplicateMatches(providers, 10)
|
|
consumers = h.deduplicateMatches(consumers, 10)
|
|
|
|
response := DirectSymbiosisResponse{
|
|
Providers: providers,
|
|
Consumers: consumers,
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// deduplicateMatches removes duplicate matches and limits the number of results
|
|
func (h *OrganizationHandler) 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
|
|
}
|
|
|
|
// GetUserOrganizations returns organizations associated with the current user
|
|
// For now, returns all organizations (can be enhanced with user-organization relationships later)
|
|
func (h *OrganizationHandler) GetUserOrganizations(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
// Get user ID from context (set by middleware)
|
|
_, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
return
|
|
}
|
|
|
|
// TODO: In future, implement user-organization relationship table
|
|
// For now, return all organizations as a temporary solution
|
|
// This allows the frontend to work while we develop proper user-org relationships
|
|
|
|
organizations, err := h.orgService.GetAll(ctx)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get organizations: %v", err)})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, organizations)
|
|
}
|