package repository import ( "context" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" "gorm.io/gorm" ) // OrganizationRepository implements domain.OrganizationRepository with GORM type OrganizationRepository struct { *BaseRepository[domain.Organization] } // NewOrganizationRepository creates a new GORM-based organization repository func NewOrganizationRepository(db *gorm.DB) domain.OrganizationRepository { return &OrganizationRepository{ BaseRepository: NewBaseRepository[domain.Organization](db), } } // GetBySector retrieves organizations by sector (NACE code or category) func (r *OrganizationRepository) GetBySector(ctx context.Context, sector domain.OrganizationSector) ([]*domain.Organization, error) { return r.FindWhereWithContext(ctx, "sector = ?", sector) } // GetBySubtype retrieves organizations by subtype func (r *OrganizationRepository) GetBySubtype(ctx context.Context, subtype domain.OrganizationSubtype) ([]*domain.Organization, error) { return r.FindWhereWithContext(ctx, "subtype = ?", subtype) } // GetWithinRadius retrieves organizations within a geographic radius func (r *OrganizationRepository) GetWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.Organization, error) { // Check if we're using PostgreSQL with PostGIS support dialector := r.DB().Dialector.Name() if dialector == "postgres" { // Use PostGIS for PostgreSQL var orgs []*domain.Organization geo := geospatial.NewGeoHelper(r.DB()) query := ` SELECT * FROM organizations WHERE location_geometry IS NOT NULL AND ` + geo.DWithinExpr("location_geometry") + ` ORDER BY ` + geo.OrderByDistanceExpr("location_geometry") + ` ` args := geo.PointRadiusArgs(lng, lat, radiusKm, true) result := r.DB().WithContext(ctx).Raw(query, args...).Scan(&orgs) if result.Error != nil { return nil, result.Error } return orgs, nil } else { // Fallback to simple bounding box approximation for other databases (SQLite, etc.) // For 10km radius, approximate delta_lat ~ 0.09, delta_lng ~ 0.15 at lat 52 var orgs []*domain.Organization query := ` SELECT * FROM organizations WHERE latitude IS NOT NULL AND longitude IS NOT NULL AND abs(latitude - ?) <= 0.09 AND abs(longitude - ?) <= 0.15 ` result := r.DB().WithContext(ctx).Raw(query, lat, lng).Scan(&orgs) if result.Error != nil { return nil, result.Error } return orgs, nil } } // GetByCertification retrieves organizations with a specific certification func (r *OrganizationRepository) GetByCertification(ctx context.Context, cert string) ([]*domain.Organization, error) { var orgs []*domain.Organization // Handle different database dialects dialect := r.DB().Dialector.Name() if dialect == "postgres" { // PostgreSQL JSONB contains operator result := r.DB().WithContext(ctx).Where("certifications @> ?", `["`+cert+`"]`).Find(&orgs) if result.Error != nil { return nil, result.Error } } else { // For SQLite and other databases, use JSON functions result := r.DB().WithContext(ctx).Where("json_extract(certifications, '$') LIKE ?", `%"`+cert+`"%`).Find(&orgs) if result.Error != nil { return nil, result.Error } } return orgs, nil } // Search performs fuzzy search on organizations using PostgreSQL pg_trgm extension // Searches across Name, Description, and Sector fields func (r *OrganizationRepository) Search(ctx context.Context, query string, limit int) ([]*domain.Organization, error) { if query == "" { return []*domain.Organization{}, nil } dialect := r.DB().Dialector.Name() var orgs []*domain.Organization if dialect == "postgres" { // Use PostgreSQL pg_trgm for fuzzy search with similarity ranking // This requires the pg_trgm extension to be enabled searchQuery := ` SELECT *, ( COALESCE(similarity(name, ?), 0) * 3 + COALESCE(similarity(COALESCE(description, ''), ?), 0) * 1 + COALESCE(similarity(COALESCE(sector, ''), ?), 0) * 2 ) AS relevance_score FROM organizations WHERE name % ? OR COALESCE(description, '') % ? OR COALESCE(sector, '') % ? ORDER BY relevance_score DESC, name ASC LIMIT ? ` result := r.DB().WithContext(ctx).Raw(searchQuery, query, query, query, query, query, query, limit).Scan(&orgs) if result.Error != nil { return nil, result.Error } } else { // Fallback for SQLite and other databases using LIKE searchPattern := "%" + query + "%" result := r.DB().WithContext(ctx). Where("name LIKE ? OR description LIKE ? OR sector LIKE ?", searchPattern, searchPattern, searchPattern). Order("name ASC"). Limit(limit). Find(&orgs) if result.Error != nil { return nil, result.Error } } return orgs, nil } // SearchSuggestions returns autocomplete suggestions for search queries // Returns unique organization names that match the query func (r *OrganizationRepository) SearchSuggestions(ctx context.Context, query string, limit int) ([]string, error) { if query == "" { return []string{}, nil } dialect := r.DB().Dialector.Name() var suggestions []string if dialect == "postgres" { // Use pg_trgm for fuzzy matching on organization names // Use subquery to handle DISTINCT with ORDER BY similarity searchQuery := ` SELECT name FROM ( SELECT DISTINCT name, similarity(name, ?) as sim_score FROM organizations WHERE name % ? ) AS distinct_names ORDER BY sim_score DESC, name ASC LIMIT ? ` result := r.DB().WithContext(ctx).Raw(searchQuery, query, query, limit).Pluck("name", &suggestions) if result.Error != nil { return nil, result.Error } } else { // Fallback for SQLite using LIKE searchPattern := "%" + query + "%" result := r.DB().WithContext(ctx). Model(&domain.Organization{}). Where("name LIKE ?", searchPattern). Distinct("name"). Order("name ASC"). Limit(limit). Pluck("name", &suggestions) if result.Error != nil { return nil, result.Error } } return suggestions, nil } // GetSectorStats returns sector statistics ordered by count descending func (r *OrganizationRepository) GetSectorStats(ctx context.Context, limit int) ([]domain.SectorStat, error) { var stats []domain.SectorStat // Use raw SQL to get sector counts result := r.DB().WithContext(ctx). Model(&domain.Organization{}). Select("sector, COUNT(*) as count"). Where("sector IS NOT NULL AND sector != ''"). Group("sector"). Order("count DESC"). Limit(limit). Find(&stats) if result.Error != nil { return nil, result.Error } return stats, nil } // GetResourceFlowsByTypeAndDirection returns resource flows by type and direction func (r *OrganizationRepository) GetResourceFlowsByTypeAndDirection(ctx context.Context, resourceType string, direction string) ([]*domain.ResourceFlow, error) { var flows []*domain.ResourceFlow result := r.DB().WithContext(ctx). Where("type = ? AND direction = ?", resourceType, direction). Find(&flows) if result.Error != nil { return nil, result.Error } return flows, nil }