turash/bugulma/backend/internal/repository/address_repository.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
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)
2025-11-25 06:01:16 +01:00

138 lines
4.0 KiB
Go

package repository
import (
"context"
"bugulma/backend/internal/domain"
"gorm.io/gorm"
)
// AddressRepository implements domain.AddressRepository with GORM
type AddressRepository struct {
*BaseRepository[domain.Address]
}
// NewAddressRepository creates a new GORM-based address repository
func NewAddressRepository(db *gorm.DB) domain.AddressRepository {
return &AddressRepository{
BaseRepository: NewBaseRepository[domain.Address](db),
}
}
// GetByID retrieves an address by ID with preloaded associations
func (r *AddressRepository) GetByID(ctx context.Context, id string) (*domain.Address, error) {
var address domain.Address
err := r.DB().WithContext(ctx).
Preload("Organizations").
Preload("Sites").
First(&address, "id = ?", id).Error
if err != nil {
return nil, handleError(err)
}
return &address, nil
}
// GetAll retrieves all addresses with preloaded associations
func (r *AddressRepository) GetAll(ctx context.Context) ([]*domain.Address, error) {
var addresses []*domain.Address
err := r.DB().WithContext(ctx).
Preload("Organizations").
Preload("Sites").
Find(&addresses).Error
return addresses, err
}
// GetWithinRadius retrieves addresses within a geographic radius
func (r *AddressRepository) GetWithinRadius(ctx context.Context, lat, lng, radiusKm float64) ([]*domain.Address, error) {
dialector := r.DB().Dialector.Name()
var addresses []*domain.Address
if dialector == "postgres" {
// Use PostGIS for PostgreSQL
query := `
SELECT * FROM addresses
WHERE latitude IS NOT NULL AND longitude IS NOT NULL
AND ST_DWithin(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography,
?
)
ORDER BY ST_Distance(
ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography,
ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography
)
`
result := r.DB().WithContext(ctx).Raw(query, lng, lat, radiusKm*1000, lng, lat).Scan(&addresses)
if result.Error != nil {
return nil, result.Error
}
} else {
// Fallback to bounding box for non-PostgreSQL databases
query := `
SELECT * FROM addresses
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(&addresses)
if result.Error != nil {
return nil, result.Error
}
}
return addresses, nil
}
// GetByOrganizationID retrieves all addresses for a specific organization
func (r *AddressRepository) GetByOrganizationID(ctx context.Context, orgID string) ([]*domain.Address, error) {
var addresses []*domain.Address
err := r.DB().WithContext(ctx).
Joins("JOIN organization_addresses ON organization_addresses.address_id = addresses.id").
Where("organization_addresses.organization_id = ?", orgID).
Find(&addresses).Error
return addresses, err
}
// GetBySiteID retrieves all addresses for a specific site
func (r *AddressRepository) GetBySiteID(ctx context.Context, siteID string) ([]*domain.Address, error) {
var addresses []*domain.Address
err := r.DB().WithContext(ctx).
Joins("JOIN site_addresses ON site_addresses.address_id = addresses.id").
Where("site_addresses.site_id = ?", siteID).
Find(&addresses).Error
return addresses, err
}
// FindOrCreate finds an existing address or creates a new one (deduplication)
func (r *AddressRepository) FindOrCreate(ctx context.Context, address *domain.Address) (*domain.Address, error) {
var existing domain.Address
// Try to find existing address by formatted address or coordinates
err := r.DB().WithContext(ctx).
Where("formatted_ru = ? OR (latitude = ? AND longitude = ?)",
address.FormattedRu, address.Latitude, address.Longitude).
First(&existing).Error
if err == nil {
// Found existing address
return &existing, nil
}
if err == gorm.ErrRecordNotFound {
// Address doesn't exist, create it
if err := r.DB().WithContext(ctx).Create(address).Error; err != nil {
return nil, handleError(err)
}
return address, nil
}
// Actual error occurred
return nil, handleError(err)
}