turash/bugulma/backend/internal/repository/graph_service_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

227 lines
6.4 KiB
Go

package repository
import (
"bugulma/backend/internal/domain"
"context"
"encoding/json"
"fmt"
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
)
// GraphServiceRepository manages Service nodes in Neo4j
type GraphServiceRepository struct {
driver neo4j.DriverWithContext
database string
}
// NewGraphServiceRepository creates a new graph service repository
func NewGraphServiceRepository(driver neo4j.DriverWithContext, dbName string) *GraphServiceRepository {
return &GraphServiceRepository{
driver: driver,
database: dbName,
}
}
// SyncToGraph syncs a service to the graph database
func (r *GraphServiceRepository) SyncToGraph(ctx context.Context, service *domain.Service) error {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeWrite,
DatabaseName: r.database,
})
defer session.Close(ctx)
// Marshal JSONB fields to JSON strings for Neo4j
certificationsJSON, _ := json.Marshal(service.Certifications)
specializationsJSON, _ := json.Marshal(service.Specializations)
sourcesJSON, _ := json.Marshal(service.Sources)
_, err := session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := `
MERGE (s:Service {id: $id})
SET s.type = $type,
s.domain = $domain,
s.description = $description,
s.on_site = $on_site,
s.hourly_rate = $hourly_rate,
s.service_area_km = $service_area_km,
s.certifications = $certifications,
s.response_time = $response_time,
s.warranty = $warranty,
s.specializations = $specializations,
s.availability = $availability,
s.sources = $sources,
s.created_at = datetime($created_at),
s.updated_at = datetime($updated_at)
WITH s
MATCH (o:Organization {id: $organization_id})
MERGE (o)-[:OFFERS]->(s)
RETURN s.id
`
params := map[string]interface{}{
"id": service.ID,
"type": string(service.Type),
"domain": service.Domain,
"description": service.Description,
"on_site": service.OnSite,
"hourly_rate": service.HourlyRate,
"service_area_km": service.ServiceAreaKm,
"certifications": string(certificationsJSON),
"response_time": service.ResponseTime,
"warranty": service.Warranty,
"specializations": string(specializationsJSON),
"availability": service.Availability,
"sources": string(sourcesJSON),
"created_at": service.CreatedAt.Format("2006-01-02T15:04:05Z"),
"updated_at": service.UpdatedAt.Format("2006-01-02T15:04:05Z"),
"organization_id": service.OrganizationID,
}
result, err := tx.Run(ctx, cypher, params)
if err != nil {
return nil, fmt.Errorf("failed to sync service to graph: %w", err)
}
if result.Next(ctx) {
return result.Record().Values[0], nil
}
return nil, fmt.Errorf("no result returned from service sync")
})
return err
}
// DeleteFromGraph removes a service from the graph database
func (r *GraphServiceRepository) DeleteFromGraph(ctx context.Context, serviceID string) error {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeWrite,
DatabaseName: r.database,
})
defer session.Close(ctx)
_, err := session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := `
MATCH (s:Service {id: $id})
DETACH DELETE s
`
_, err := tx.Run(ctx, cypher, map[string]interface{}{
"id": serviceID,
})
return nil, err
})
return err
}
// FindMatchingServices finds services that could match a given need
func (r *GraphServiceRepository) FindMatchingServices(ctx context.Context, serviceType domain.ServiceType, domainName string, maxHourlyRate float64) ([]map[string]interface{}, error) {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeRead,
DatabaseName: r.database,
})
defer session.Close(ctx)
result, err := session.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := `
MATCH (s:Service)
WHERE s.type = $service_type
AND s.domain CONTAINS $domain_name
AND (s.hourly_rate <= $max_hourly_rate OR s.hourly_rate IS NULL)
RETURN s {
.*,
organization: [(s)<-[:OFFERS]-(o:Organization) | o {id: o.id, name: o.name}][0]
} as service
ORDER BY s.hourly_rate ASC
LIMIT 50
`
result, err := tx.Run(ctx, cypher, map[string]interface{}{
"service_type": string(serviceType),
"domain_name": domainName,
"max_hourly_rate": maxHourlyRate,
})
if err != nil {
return nil, err
}
var services []map[string]interface{}
for result.Next(ctx) {
record := result.Record()
if service, ok := record.Get("service"); ok {
if serviceMap, ok := service.(map[string]interface{}); ok {
services = append(services, serviceMap)
}
}
}
return services, nil
})
if err != nil {
return nil, err
}
if services, ok := result.([]map[string]interface{}); ok {
return services, nil
}
return []map[string]interface{}{}, nil
}
// FindServiceProviders finds organizations that offer services in a specific domain within a geographic area
func (r *GraphServiceRepository) FindServiceProviders(ctx context.Context, domainName string, maxDistanceKm float64) ([]map[string]interface{}, error) {
session := r.driver.NewSession(ctx, neo4j.SessionConfig{
AccessMode: neo4j.AccessModeRead,
DatabaseName: r.database,
})
defer session.Close(ctx)
result, err := session.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (interface{}, error) {
cypher := `
MATCH (o:Organization)-[:OFFERS]->(s:Service)
WHERE s.domain CONTAINS $domain_name
AND s.service_area_km >= $max_distance_km
RETURN o {
id: o.id,
name: o.name,
services: [(o)-[:OFFERS]->(s) WHERE s.domain CONTAINS $domain_name | s {.*}][..10]
} as organization
ORDER BY o.name
LIMIT 25
`
result, err := tx.Run(ctx, cypher, map[string]interface{}{
"domain_name": domainName,
"max_distance_km": maxDistanceKm,
})
if err != nil {
return nil, err
}
var organizations []map[string]interface{}
for result.Next(ctx) {
record := result.Record()
if org, ok := record.Get("organization"); ok {
if orgMap, ok := org.(map[string]interface{}); ok {
organizations = append(organizations, orgMap)
}
}
}
return organizations, nil
})
if err != nil {
return nil, err
}
if organizations, ok := result.([]map[string]interface{}); ok {
return organizations, nil
}
return []map[string]interface{}{}, nil
}