turash/bugulma/backend/internal/handler/organization_handler.go

809 lines
24 KiB
Go

package handler
import (
"context"
"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
proposalService *service.ProposalService
}
func NewOrganizationHandler(orgService *service.OrganizationService, imageService *service.ImageService, resourceFlowService *service.ResourceFlowService, matchingService *matching.Service, proposalService *service.ProposalService) *OrganizationHandler {
return &OrganizationHandler{
orgService: orgService,
imageService: imageService,
resourceFlowService: resourceFlowService,
matchingService: matchingService,
proposalService: proposalService,
}
}
// Helper methods for error responses
func (h *OrganizationHandler) errorResponse(c *gin.Context, status int, message string) {
c.JSON(status, gin.H{"error": message})
}
func (h *OrganizationHandler) internalError(c *gin.Context, err error) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
func (h *OrganizationHandler) notFound(c *gin.Context, resource string) {
c.JSON(http.StatusNotFound, gin.H{"error": resource + " not found"})
}
func (h *OrganizationHandler) badRequest(c *gin.Context, err error) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
// Helper to parse limit query parameter with validation
func (h *OrganizationHandler) parseLimitQuery(c *gin.Context, defaultLimit, maxLimit int) int {
limitStr := c.DefaultQuery("limit", strconv.Itoa(defaultLimit))
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 1 {
return defaultLimit
}
if limit > maxLimit {
return maxLimit
}
return limit
}
// Helper to get organization by ID or return error response
func (h *OrganizationHandler) getOrgByIDOrError(c *gin.Context, id string) (*domain.Organization, bool) {
org, err := h.orgService.GetByID(c.Request.Context(), id)
if err != nil {
h.notFound(c, "Organization")
return nil, false
}
return org, true
}
// Helper to convert subtypes to string slice
func subtypesToStrings(subtypes []domain.OrganizationSubtype) []string {
result := make([]string, len(subtypes))
for i, st := range subtypes {
result[i] = string(st)
}
return result
}
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 {
h.badRequest(c, err)
return
}
// Validate subtype
subtype := domain.OrganizationSubtype(req.Subtype)
if !domain.IsValidSubtype(subtype) {
h.errorResponse(c, http.StatusBadRequest, "invalid subtype: "+req.Subtype)
return
}
orgReq := service.CreateOrganizationRequest{
Name: req.Name,
Subtype: subtype,
Sector: domain.OrganizationSector(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 {
h.internalError(c, err)
return
}
c.JSON(http.StatusCreated, org)
}
func (h *OrganizationHandler) GetByID(c *gin.Context) {
id := c.Param("id")
org, ok := h.getOrgByIDOrError(c, id)
if !ok {
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(), domain.OrganizationSector(sector))
if err != nil {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, orgs)
return
}
// No filter - return all organizations
orgs, err := h.orgService.GetAll(c.Request.Context())
if err != nil {
h.internalError(c, err)
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 {
h.badRequest(c, err)
return
}
org, ok := h.getOrgByIDOrError(c, id)
if !ok {
return
}
// Validate subtype
subtype := domain.OrganizationSubtype(req.Subtype)
if !domain.IsValidSubtype(subtype) {
h.errorResponse(c, http.StatusBadRequest, "invalid subtype: "+req.Subtype)
return
}
// Update fields
org.Name = req.Name
org.Sector = domain.OrganizationSector(req.Sector)
org.Subtype = subtype
org.Description = req.Description
org.LogoURL = req.LogoURL
org.Website = req.Website
if err := h.orgService.Update(c.Request.Context(), org); err != nil {
h.internalError(c, err)
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 {
h.notFound(c, "Organization")
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 {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, orgs)
}
func (h *OrganizationHandler) GetBySector(c *gin.Context) {
sectorParam := c.Param("sector")
sector := domain.OrganizationSector(sectorParam)
orgs, err := h.orgService.GetBySector(c.Request.Context(), sector)
if err != nil {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, orgs)
}
// GetSectorStats returns sector statistics (top sectors by organization count)
func (h *OrganizationHandler) GetSectorStats(c *gin.Context) {
limit := h.parseLimitQuery(c, 10, 50)
stats, err := h.orgService.GetSectorStats(c.Request.Context(), limit)
if err != nil {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"sectors": stats})
}
// GetAllSubtypes returns all available organization subtypes
func (h *OrganizationHandler) GetAllSubtypes(c *gin.Context) {
subtypes := domain.GetAllSubtypes()
c.JSON(http.StatusOK, gin.H{"subtypes": subtypesToStrings(subtypes)})
}
// GetSubtypesBySector returns subtypes filtered by sector
func (h *OrganizationHandler) GetSubtypesBySector(c *gin.Context) {
sectorParam := c.Query("sector")
if sectorParam == "" {
h.GetAllSubtypes(c)
return
}
// Parse sector parameter as OrganizationSector enum
sector := domain.OrganizationSector(sectorParam)
subtypes := domain.GetSubtypesBySector(sector)
c.JSON(http.StatusOK, gin.H{
"sector": sector,
"subtypes": subtypesToStrings(subtypes),
})
}
func (h *OrganizationHandler) GetByCertification(c *gin.Context) {
cert := c.Param("cert")
orgs, err := h.orgService.GetByCertification(c.Request.Context(), cert)
if err != nil {
h.internalError(c, err)
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 == "" {
h.errorResponse(c, http.StatusBadRequest, "query parameter 'q' is required")
return
}
limit := h.parseLimitQuery(c, 50, 200)
orgs, err := h.orgService.Search(c.Request.Context(), query, limit)
if err != nil {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"organizations": orgs,
"count": len(orgs),
"total": len(orgs), // For now, we don't have total count from search
})
}
// 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 := h.parseLimitQuery(c, 10, 50)
suggestions, err := h.orgService.SearchSuggestions(c.Request.Context(), query, limit)
if err != nil {
h.internalError(c, err)
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 {
h.badRequest(c, err)
return
}
orgs, err := h.orgService.GetWithinRadius(c.Request.Context(), query.Latitude, query.Longitude, query.RadiusKm)
if err != nil {
h.internalError(c, err)
return
}
c.JSON(http.StatusOK, orgs)
}
// UploadLogo handles logo image uploads for organizations
func (h *OrganizationHandler) UploadLogo(c *gin.Context) {
orgID := c.Param("id")
uploadedImage, ok := h.handleImageUpload(c, orgID, "logo", "logos")
if !ok {
return
}
org, ok := h.getOrgByIDOrError(c, orgID)
if !ok {
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 {
h.errorResponse(c, http.StatusInternalServerError, "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 {
h.errorResponse(c, http.StatusBadRequest, "No image file provided")
return
}
defer file.Close()
// Save the image
uploadedImage, err := h.imageService.SaveImage(file, header, "gallery")
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to save image: %v", err))
return
}
// Update the organization by adding the image to gallery
org, ok := h.getOrgByIDOrError(c, orgID)
if !ok {
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, &currentImages); 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 == "" {
h.errorResponse(c, http.StatusBadRequest, "Image URL required")
return
}
// Get the organization
org, ok := h.getOrgByIDOrError(c, orgID)
if !ok {
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, &currentImages); 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 {
h.errorResponse(c, http.StatusInternalServerError, "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")
limit := h.parseLimitQuery(c, 5, 50)
org, ok := h.getOrgByIDOrError(c, orgID)
if !ok {
return
}
// Get organizations by sector (primary similarity factor)
sectorOrgs, err := h.orgService.GetBySector(c.Request.Context(), org.Sector)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to get similar organizations")
return
}
// Get resource flows for the organization to find complementary organizations
orgFlows, err := h.resourceFlowService.GetByOrganizationID(c.Request.Context(), orgID)
if err != nil {
// Continue without resource flow matching if it fails
orgFlows = []*domain.ResourceFlow{}
}
// Calculate similarity scores using service layer
similarOrgs, err := h.orgService.CalculateSimilarityScores(c.Request.Context(), orgID, sectorOrgs, orgFlows)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to calculate similarity scores")
return
}
// Limit results
if len(similarOrgs) > limit {
similarOrgs = similarOrgs[:limit]
}
c.JSON(http.StatusOK, similarOrgs)
}
// GetOrganizationProposals returns proposals related to the organization
func (h *OrganizationHandler) GetOrganizationProposals(c *gin.Context) {
orgID := c.Param("id")
if h.proposalService == nil {
h.errorResponse(c, http.StatusServiceUnavailable, "Proposal service is not available")
return
}
proposals, err := h.proposalService.GetByOrganizationID(c.Request.Context(), orgID)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to get organization proposals")
return
}
c.JSON(http.StatusOK, proposals)
}
// GetOrganizationResources returns resource flows for the organization
func (h *OrganizationHandler) GetOrganizationResources(c *gin.Context) {
orgID := c.Param("id")
resourceFlows, err := h.resourceFlowService.GetByOrganizationID(c.Request.Context(), orgID)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to get organization resources")
return
}
c.JSON(http.StatusOK, resourceFlows)
}
// GetOrganizationProducts returns products for the organization
func (h *OrganizationHandler) GetOrganizationProducts(c *gin.Context) {
orgID := c.Param("id")
if h.matchingService == nil {
h.errorResponse(c, http.StatusServiceUnavailable, "Matching service is not available")
return
}
ctx := c.Request.Context()
products, err := h.matchingService.GetProductsByOrganization(ctx, orgID)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to get organization products")
return
}
matches, err := h.convertItemsToDiscoveryMatches(ctx, products, "product")
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to convert products to matches")
return
}
c.JSON(http.StatusOK, matches)
}
// GetOrganizationServices returns services for the organization
func (h *OrganizationHandler) GetOrganizationServices(c *gin.Context) {
orgID := c.Param("id")
if h.matchingService == nil {
h.errorResponse(c, http.StatusServiceUnavailable, "Matching service is not available")
return
}
ctx := c.Request.Context()
services, err := h.matchingService.GetServicesByOrganization(ctx, orgID)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to get organization services")
return
}
matches, err := h.convertItemsToDiscoveryMatches(ctx, services, "service")
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to convert services to matches")
return
}
c.JSON(http.StatusOK, matches)
}
// GetDirectMatches returns direct matches for the organization
func (h *OrganizationHandler) GetDirectMatches(c *gin.Context) {
orgID := c.Param("id")
if orgID == "" {
h.errorResponse(c, http.StatusBadRequest, "Organization ID is required")
return
}
ctx := c.Request.Context()
resourceFlows, err := h.resourceFlowService.GetByOrganizationID(ctx, orgID)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to get resource flows: %v", err))
return
}
providers, consumers, err := h.orgService.FindDirectMatches(ctx, orgID, resourceFlows, 10)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, "Failed to find direct matches")
return
}
response := service.DirectSymbiosisResponse{
Providers: providers,
Consumers: consumers,
}
c.JSON(http.StatusOK, response)
}
// convertItemsToDiscoveryMatches converts products or services to DiscoveryMatch format
func (h *OrganizationHandler) convertItemsToDiscoveryMatches(ctx context.Context, items interface{}, matchType string) ([]*matching.DiscoveryMatch, error) {
var matches []*matching.DiscoveryMatch
switch matchType {
case "product":
products, ok := items.([]*domain.Product)
if !ok {
return nil, fmt.Errorf("invalid products type")
}
for _, product := range products {
var org *domain.Organization
var site *domain.Site
if product.OrganizationID != "" {
org, _ = h.orgService.GetByID(ctx, product.OrganizationID)
}
// Note: Would need site service/repo access - for now, skip site
match := &matching.DiscoveryMatch{
Product: product,
MatchType: "product",
RelevanceScore: 1.0,
TextMatchScore: 1.0,
CategoryMatchScore: 1.0,
DistanceScore: 1.0,
PriceMatchScore: 1.0,
AvailabilityScore: 1.0,
Organization: org,
Site: site,
}
matches = append(matches, match)
}
case "service":
services, ok := items.([]*domain.Service)
if !ok {
return nil, fmt.Errorf("invalid services type")
}
for _, service := range services {
var org *domain.Organization
var site *domain.Site
if service.OrganizationID != "" {
org, _ = h.orgService.GetByID(ctx, service.OrganizationID)
}
// Note: Would need site service/repo access - for now, skip site
match := &matching.DiscoveryMatch{
Service: service,
MatchType: "service",
RelevanceScore: 1.0,
TextMatchScore: 1.0,
CategoryMatchScore: 1.0,
DistanceScore: 1.0,
PriceMatchScore: 1.0,
AvailabilityScore: 1.0,
Organization: org,
Site: site,
}
matches = append(matches, match)
}
default:
return nil, fmt.Errorf("unsupported match type: %s", matchType)
}
return matches, nil
}
// handleImageUpload handles common image upload logic
func (h *OrganizationHandler) handleImageUpload(c *gin.Context, orgID, formField, folderName string) (*service.UploadedImage, bool) {
file, header, err := c.Request.FormFile(formField)
if err != nil {
h.errorResponse(c, http.StatusBadRequest, "No "+formField+" file provided")
return nil, false
}
defer file.Close()
uploadedImage, err := h.imageService.SaveImage(file, header, folderName)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to save %s: %v", formField, err))
return nil, false
}
return uploadedImage, true
}
// 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 {
h.errorResponse(c, http.StatusUnauthorized, "User not authenticated")
return
}
// TODO: In future, implement user-organization relationship table
// For now, return all organizations as a temporary solution
organizations, err := h.orgService.GetAll(ctx)
if err != nil {
h.errorResponse(c, http.StatusInternalServerError, fmt.Sprintf("Failed to get organizations: %v", err))
return
}
c.JSON(http.StatusOK, organizations)
}