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)
211 lines
6.3 KiB
Go
211 lines
6.3 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
|
|
"bugulma/backend/internal/domain"
|
|
|
|
"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 string) ([]*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
|
|
query := `
|
|
SELECT * FROM organizations
|
|
WHERE location_geometry IS NOT NULL
|
|
AND ST_DWithin(
|
|
location_geometry::geography,
|
|
ST_GeogFromText('POINT(? ?)'),
|
|
? * 1000
|
|
)
|
|
ORDER BY location_geometry <-> ST_GeogFromText('POINT(? ?)')
|
|
`
|
|
result := r.DB().WithContext(ctx).Raw(query, lng, lat, radiusKm, lng, lat).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
|
|
}
|