turash/bugulma/backend/internal/repository/organization_repository.go

224 lines
6.9 KiB
Go

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
}