package repository import ( "context" "bugulma/backend/internal/domain" "bugulma/backend/internal/geospatial" "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 geo := geospatial.NewGeoHelper(r.DB()) query := ` SELECT * FROM addresses WHERE latitude IS NOT NULL AND longitude IS NOT NULL AND ` + geo.DWithinExpr("ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)") + ` ORDER BY ST_Distance( ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)::geography, ` + geo.PointExpr() + `::geography ) ` // Use includeOrderBy=true so PointRadiusArgs returns args in the order // [lng, lat, radiusKm, lng, lat] matching DWithin + ORDER BY placeholders args := geo.PointRadiusArgs(lng, lat, radiusKm, true) result := r.DB().WithContext(ctx).Raw(query, args...).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) }