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) tagsJSON, _ := json.Marshal(service.Tags) availabilityScheduleJSON, _ := json.Marshal(service.AvailabilitySchedule) _, 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.search_keywords = $search_keywords, s.tags = $tags, s.availability_status = $availability_status, s.availability_schedule = $availability_schedule, s.site_id = $site_id, s.created_at = datetime($created_at), s.updated_at = datetime($updated_at) WITH s MATCH (o:Organization {id: $organization_id}) MERGE (o)-[:OFFERS]->(s) WITH s, o OPTIONAL MATCH (st:Site {id: $site_id}) FOREACH (x IN CASE WHEN st IS NOT NULL THEN [1] ELSE [] END | MERGE (st)-[:HOSTS]->(s) ) RETURN s.id ` var siteID interface{} if service.SiteID != nil { siteID = *service.SiteID } var availabilitySchedule interface{} if service.AvailabilitySchedule != nil { availabilitySchedule = string(availabilityScheduleJSON) } 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), "search_keywords": service.SearchKeywords, "tags": string(tagsJSON), "availability_status": service.AvailabilityStatus, "availability_schedule": availabilitySchedule, "site_id": siteID, "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 }