Unify hub offer queries and drop radius filters
All checks were successful
Build Docker Image / build (push) Successful in 2m7s
All checks were successful
Build Docker Image / build (push) Successful in 2m7s
This commit is contained in:
@@ -311,7 +311,7 @@ class Query(graphene.ObjectType):
|
|||||||
description="Get hubs where a product is available nearby",
|
description="Get hubs where a product is available nearby",
|
||||||
)
|
)
|
||||||
|
|
||||||
offers_by_hub = graphene.List(
|
offers_for_hub = graphene.List(
|
||||||
ProductRouteOptionType,
|
ProductRouteOptionType,
|
||||||
hub_uuid=graphene.String(required=True),
|
hub_uuid=graphene.String(required=True),
|
||||||
product_uuid=graphene.String(required=True),
|
product_uuid=graphene.String(required=True),
|
||||||
@@ -319,13 +319,6 @@ class Query(graphene.ObjectType):
|
|||||||
description="Get offers for a product with routes to hub (auto → rail* → auto)",
|
description="Get offers for a product with routes to hub (auto → rail* → auto)",
|
||||||
)
|
)
|
||||||
|
|
||||||
offer_to_hub = graphene.Field(
|
|
||||||
ProductRouteOptionType,
|
|
||||||
offer_uuid=graphene.String(required=True),
|
|
||||||
hub_uuid=graphene.String(required=True),
|
|
||||||
description="Get route from a specific offer to hub",
|
|
||||||
)
|
|
||||||
|
|
||||||
# New unified endpoints (coordinate-based)
|
# New unified endpoints (coordinate-based)
|
||||||
nearest_hubs = graphene.List(
|
nearest_hubs = graphene.List(
|
||||||
NodeType,
|
NodeType,
|
||||||
@@ -1124,48 +1117,8 @@ class Query(graphene.ObjectType):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def resolve_hubs_for_product(self, info, product_uuid, radius_km=500):
|
def resolve_hubs_for_product(self, info, product_uuid, radius_km=500):
|
||||||
"""Get hubs that have this product available within radius."""
|
"""Get hubs that can accept this product (graph-based)."""
|
||||||
db = get_db()
|
return self.resolve_hubs_for_product_graph(info, product_uuid)
|
||||||
aql = """
|
|
||||||
FOR offer IN nodes
|
|
||||||
FILTER offer.node_type == 'offer'
|
|
||||||
FILTER offer.product_uuid == @product_uuid
|
|
||||||
FILTER offer.latitude != null AND offer.longitude != null
|
|
||||||
FOR hub IN nodes
|
|
||||||
FILTER hub.node_type == 'logistics' OR hub.node_type == null
|
|
||||||
FILTER hub.latitude != null AND hub.longitude != null
|
|
||||||
LET dist = DISTANCE(offer.latitude, offer.longitude, hub.latitude, hub.longitude) / 1000
|
|
||||||
FILTER dist <= @radius_km
|
|
||||||
COLLECT hub_uuid = hub._key, hub_name = hub.name,
|
|
||||||
hub_lat = hub.latitude, hub_lon = hub.longitude,
|
|
||||||
hub_country = hub.country, hub_country_code = hub.country_code,
|
|
||||||
hub_transport = hub.transport_types
|
|
||||||
RETURN {
|
|
||||||
uuid: hub_uuid,
|
|
||||||
name: hub_name,
|
|
||||||
latitude: hub_lat,
|
|
||||||
longitude: hub_lon,
|
|
||||||
country: hub_country,
|
|
||||||
country_code: hub_country_code,
|
|
||||||
transport_types: hub_transport
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cursor = db.aql.execute(aql, bind_vars={'product_uuid': product_uuid, 'radius_km': radius_km})
|
|
||||||
hubs = [NodeType(
|
|
||||||
uuid=h['uuid'],
|
|
||||||
name=h['name'],
|
|
||||||
latitude=h['latitude'],
|
|
||||||
longitude=h['longitude'],
|
|
||||||
country=h['country'],
|
|
||||||
country_code=h.get('country_code'),
|
|
||||||
transport_types=h.get('transport_types')
|
|
||||||
) for h in cursor]
|
|
||||||
logger.info("Found %d hubs for product %s", len(hubs), product_uuid)
|
|
||||||
return hubs
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error getting hubs for product: %s", e)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def resolve_hubs_for_product_graph(self, info, product_uuid, limit=500):
|
def resolve_hubs_for_product_graph(self, info, product_uuid, limit=500):
|
||||||
"""Get hubs that can be reached by graph routes for a product."""
|
"""Get hubs that can be reached by graph routes for a product."""
|
||||||
@@ -1219,7 +1172,11 @@ class Query(graphene.ObjectType):
|
|||||||
logger.error("Error getting graph hubs for product %s: %s", product_uuid, e)
|
logger.error("Error getting graph hubs for product %s: %s", product_uuid, e)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def resolve_offers_by_hub(self, info, hub_uuid, product_uuid, limit=10):
|
def resolve_offers_for_hub(self, info, hub_uuid, product_uuid, limit=10):
|
||||||
|
"""Alias for offers by hub (graph-based)."""
|
||||||
|
return self.resolve_offers_by_hub(info, hub_uuid, product_uuid, limit=limit)
|
||||||
|
|
||||||
|
def resolve_offers_by_hub(self, info, hub_uuid, product_uuid=None, limit=10):
|
||||||
"""
|
"""
|
||||||
Get offers for a product with routes to hub.
|
Get offers for a product with routes to hub.
|
||||||
Uses phase-based routing: auto → rail* → auto
|
Uses phase-based routing: auto → rail* → auto
|
||||||
@@ -1335,9 +1292,11 @@ class Query(graphene.ObjectType):
|
|||||||
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Found an offer for the product
|
# Found an offer (optionally filtered by product)
|
||||||
node_doc = node_docs.get(node_key)
|
node_doc = node_docs.get(node_key)
|
||||||
if node_doc and node_doc.get('product_uuid') == product_uuid:
|
if node_doc and node_doc.get('node_type') == 'offer' and (
|
||||||
|
product_uuid is None or node_doc.get('product_uuid') == product_uuid
|
||||||
|
):
|
||||||
path_edges = []
|
path_edges = []
|
||||||
state = (node_key, phase)
|
state = (node_key, phase)
|
||||||
current_key = node_key
|
current_key = node_key
|
||||||
@@ -1393,176 +1352,15 @@ class Query(graphene.ObjectType):
|
|||||||
predecessors[state_key] = ((node_key, phase), neighbor)
|
predecessors[state_key] = ((node_key, phase), neighbor)
|
||||||
|
|
||||||
if not found_routes:
|
if not found_routes:
|
||||||
logger.info("No offers found for product %s near hub %s", product_uuid, hub_uuid)
|
logger.info("No offers found near hub %s", hub_uuid)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
logger.info("Found %d offers for product %s near hub %s", len(found_routes), product_uuid, hub_uuid)
|
if product_uuid:
|
||||||
|
logger.info("Found %d offers for product %s near hub %s", len(found_routes), product_uuid, hub_uuid)
|
||||||
|
else:
|
||||||
|
logger.info("Found %d offers near hub %s", len(found_routes), hub_uuid)
|
||||||
return found_routes
|
return found_routes
|
||||||
|
|
||||||
def resolve_offer_to_hub(self, info, offer_uuid, hub_uuid):
|
|
||||||
"""
|
|
||||||
Get route from a specific offer to hub.
|
|
||||||
Uses phase-based routing: auto → rail* → auto
|
|
||||||
"""
|
|
||||||
db = get_db()
|
|
||||||
ensure_graph()
|
|
||||||
nodes_col = db.collection('nodes')
|
|
||||||
|
|
||||||
offer = nodes_col.get(offer_uuid)
|
|
||||||
if not offer:
|
|
||||||
logger.info("Offer %s not found", offer_uuid)
|
|
||||||
return None
|
|
||||||
|
|
||||||
hub = nodes_col.get(hub_uuid)
|
|
||||||
if not hub:
|
|
||||||
logger.info("Hub %s not found", hub_uuid)
|
|
||||||
return None
|
|
||||||
|
|
||||||
hub_lat = hub.get('latitude')
|
|
||||||
hub_lon = hub.get('longitude')
|
|
||||||
offer_lat = offer.get('latitude')
|
|
||||||
offer_lon = offer.get('longitude')
|
|
||||||
|
|
||||||
# Phase-based routing from hub to offer
|
|
||||||
def allowed_next_phase(current_phase, transport_type):
|
|
||||||
if current_phase == 'end_auto':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer'
|
|
||||||
if transport_type == 'auto':
|
|
||||||
return 'end_auto_done'
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
return None
|
|
||||||
if current_phase == 'end_auto_done':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer'
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
return None
|
|
||||||
if current_phase == 'rail':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer'
|
|
||||||
if transport_type == 'rail':
|
|
||||||
return 'rail'
|
|
||||||
if transport_type == 'auto':
|
|
||||||
return 'start_auto_done'
|
|
||||||
return None
|
|
||||||
if current_phase == 'start_auto_done':
|
|
||||||
if transport_type == 'offer':
|
|
||||||
return 'offer'
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def fetch_neighbors(node_key, phase):
|
|
||||||
if phase == 'end_auto':
|
|
||||||
types = ['auto', 'rail', 'offer']
|
|
||||||
elif phase == 'end_auto_done':
|
|
||||||
types = ['rail', 'offer']
|
|
||||||
elif phase == 'rail':
|
|
||||||
types = ['rail', 'auto', 'offer']
|
|
||||||
elif phase == 'start_auto_done':
|
|
||||||
types = ['offer']
|
|
||||||
else:
|
|
||||||
types = ['offer']
|
|
||||||
|
|
||||||
aql = """
|
|
||||||
FOR edge IN edges
|
|
||||||
FILTER edge.transport_type IN @types
|
|
||||||
FILTER edge._from == @node_id OR edge._to == @node_id
|
|
||||||
LET neighbor_id = edge._from == @node_id ? edge._to : edge._from
|
|
||||||
LET neighbor = DOCUMENT(neighbor_id)
|
|
||||||
FILTER neighbor != null
|
|
||||||
RETURN {
|
|
||||||
neighbor_key: neighbor._key,
|
|
||||||
neighbor_doc: neighbor,
|
|
||||||
from_id: edge._from,
|
|
||||||
to_id: edge._to,
|
|
||||||
transport_type: edge.transport_type,
|
|
||||||
distance_km: edge.distance_km,
|
|
||||||
travel_time_seconds: edge.travel_time_seconds
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
cursor = db.aql.execute(
|
|
||||||
aql,
|
|
||||||
bind_vars={
|
|
||||||
'node_id': f"nodes/{node_key}",
|
|
||||||
'types': types,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return list(cursor)
|
|
||||||
|
|
||||||
queue = []
|
|
||||||
counter = 0
|
|
||||||
heapq.heappush(queue, (0, counter, hub_uuid, 'end_auto'))
|
|
||||||
|
|
||||||
visited = {}
|
|
||||||
predecessors = {}
|
|
||||||
node_docs = {hub_uuid: hub, offer_uuid: offer}
|
|
||||||
|
|
||||||
expansions = 0
|
|
||||||
|
|
||||||
while queue and expansions < Query.MAX_EXPANSIONS:
|
|
||||||
cost, _, node_key, phase = heapq.heappop(queue)
|
|
||||||
|
|
||||||
if (node_key, phase) in visited and cost > visited[(node_key, phase)]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Found the specific offer
|
|
||||||
if node_key == offer_uuid:
|
|
||||||
path_edges = []
|
|
||||||
state = (node_key, phase)
|
|
||||||
current_key = node_key
|
|
||||||
while state in predecessors:
|
|
||||||
prev_state, edge_info = predecessors[state]
|
|
||||||
prev_key = prev_state[0]
|
|
||||||
path_edges.append((current_key, prev_key, edge_info))
|
|
||||||
state = prev_state
|
|
||||||
current_key = prev_key
|
|
||||||
|
|
||||||
route = _build_route_from_edges(path_edges, node_docs)
|
|
||||||
distance_km = None
|
|
||||||
if offer_lat is not None and offer_lon is not None and hub_lat is not None and hub_lon is not None:
|
|
||||||
distance_km = _distance_km(offer_lat, offer_lon, hub_lat, hub_lon)
|
|
||||||
|
|
||||||
return ProductRouteOptionType(
|
|
||||||
source_uuid=offer_uuid,
|
|
||||||
source_name=offer.get('name') or offer.get('product_name'),
|
|
||||||
source_lat=offer_lat,
|
|
||||||
source_lon=offer_lon,
|
|
||||||
distance_km=distance_km,
|
|
||||||
routes=[route] if route else [],
|
|
||||||
)
|
|
||||||
|
|
||||||
neighbors = fetch_neighbors(node_key, phase)
|
|
||||||
expansions += 1
|
|
||||||
|
|
||||||
for neighbor in neighbors:
|
|
||||||
transport_type = neighbor.get('transport_type')
|
|
||||||
next_phase = allowed_next_phase(phase, transport_type)
|
|
||||||
if next_phase is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
travel_time = neighbor.get('travel_time_seconds')
|
|
||||||
distance_km = neighbor.get('distance_km')
|
|
||||||
|
|
||||||
neighbor_key = neighbor.get('neighbor_key')
|
|
||||||
node_docs[neighbor_key] = neighbor.get('neighbor_doc')
|
|
||||||
|
|
||||||
step_cost = travel_time if travel_time is not None else (distance_km or 0)
|
|
||||||
new_cost = cost + step_cost
|
|
||||||
|
|
||||||
state_key = (neighbor_key, next_phase)
|
|
||||||
if state_key in visited and new_cost >= visited[state_key]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited[state_key] = new_cost
|
|
||||||
counter += 1
|
|
||||||
heapq.heappush(queue, (new_cost, counter, neighbor_key, next_phase))
|
|
||||||
predecessors[state_key] = ((node_key, phase), neighbor)
|
|
||||||
|
|
||||||
logger.info("No route found from offer %s to hub %s", offer_uuid, hub_uuid)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, source_uuid=None, use_graph=False, limit=12):
|
def resolve_nearest_hubs(self, info, lat, lon, radius=1000, product_uuid=None, source_uuid=None, use_graph=False, limit=12):
|
||||||
"""Find nearest hubs to coordinates, optionally filtered by product availability."""
|
"""Find nearest hubs to coordinates, optionally filtered by product availability."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
@@ -1654,51 +1452,23 @@ class Query(graphene.ObjectType):
|
|||||||
))
|
))
|
||||||
return hubs
|
return hubs
|
||||||
|
|
||||||
if product_uuid and use_graph:
|
if product_uuid:
|
||||||
return self.resolve_hubs_for_product_graph(info, product_uuid, limit=limit)
|
return self.resolve_hubs_for_product_graph(info, product_uuid, limit=limit)
|
||||||
|
|
||||||
if product_uuid:
|
# Simple nearest hubs search (no radius filtering)
|
||||||
# Find hubs that have offers for this product within radius
|
aql = """
|
||||||
aql = """
|
FOR hub IN nodes
|
||||||
FOR offer IN nodes
|
FILTER hub.node_type == 'logistics' OR hub.node_type == null
|
||||||
FILTER offer.node_type == 'offer'
|
FILTER hub.product_uuid == null
|
||||||
FILTER offer.product_uuid == @product_uuid
|
LET types = hub.transport_types != null ? hub.transport_types : []
|
||||||
FILTER offer.latitude != null AND offer.longitude != null
|
FILTER ('rail' IN types) OR ('sea' IN types)
|
||||||
LET dist_to_offer = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
FILTER hub.latitude != null AND hub.longitude != null
|
||||||
FILTER dist_to_offer <= @radius
|
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
|
||||||
|
SORT dist ASC
|
||||||
FOR hub IN nodes
|
LIMIT @limit
|
||||||
FILTER hub.node_type == 'logistics' OR hub.node_type == null
|
RETURN MERGE(hub, {distance_km: dist})
|
||||||
FILTER hub.product_uuid == null
|
"""
|
||||||
LET types = hub.transport_types != null ? hub.transport_types : []
|
bind_vars = {'lat': lat, 'lon': lon, 'limit': limit}
|
||||||
FILTER ('rail' IN types) OR ('sea' IN types)
|
|
||||||
FILTER hub.latitude != null AND hub.longitude != null
|
|
||||||
LET dist_to_hub = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
|
|
||||||
FILTER dist_to_hub <= @radius
|
|
||||||
COLLECT hub_uuid = hub._key INTO hub_group
|
|
||||||
LET first_hub = FIRST(hub_group)[0].hub
|
|
||||||
LET hub_dist = DISTANCE(first_hub.latitude, first_hub.longitude, @lat, @lon) / 1000
|
|
||||||
SORT hub_dist ASC
|
|
||||||
LIMIT @limit
|
|
||||||
RETURN MERGE(first_hub, {_key: hub_uuid, distance_km: hub_dist})
|
|
||||||
"""
|
|
||||||
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'product_uuid': product_uuid, 'limit': limit}
|
|
||||||
else:
|
|
||||||
# Simple nearest hubs search
|
|
||||||
aql = """
|
|
||||||
FOR hub IN nodes
|
|
||||||
FILTER hub.node_type == 'logistics' OR hub.node_type == null
|
|
||||||
FILTER hub.product_uuid == null
|
|
||||||
LET types = hub.transport_types != null ? hub.transport_types : []
|
|
||||||
FILTER ('rail' IN types) OR ('sea' IN types)
|
|
||||||
FILTER hub.latitude != null AND hub.longitude != null
|
|
||||||
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
|
|
||||||
FILTER dist <= @radius
|
|
||||||
SORT dist ASC
|
|
||||||
LIMIT @limit
|
|
||||||
RETURN MERGE(hub, {distance_km: dist})
|
|
||||||
"""
|
|
||||||
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor = db.aql.execute(aql, bind_vars=bind_vars)
|
cursor = db.aql.execute(aql, bind_vars=bind_vars)
|
||||||
@@ -1716,7 +1486,7 @@ class Query(graphene.ObjectType):
|
|||||||
edges=[],
|
edges=[],
|
||||||
distance_km=node.get('distance_km'),
|
distance_km=node.get('distance_km'),
|
||||||
))
|
))
|
||||||
logger.info("Found %d hubs near (%.3f, %.3f) within %d km", len(hubs), lat, lon, radius)
|
logger.info("Found %d hubs near (%.3f, %.3f)", len(hubs), lat, lon)
|
||||||
return hubs
|
return hubs
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error finding nearest hubs: %s", e)
|
logger.error("Error finding nearest hubs: %s", e)
|
||||||
@@ -1727,91 +1497,41 @@ class Query(graphene.ObjectType):
|
|||||||
db = get_db()
|
db = get_db()
|
||||||
ensure_graph()
|
ensure_graph()
|
||||||
|
|
||||||
# If hub_uuid + product_uuid provided, use graph search to return only offers with routes.
|
|
||||||
if hub_uuid and product_uuid:
|
|
||||||
try:
|
|
||||||
nodes_col = db.collection('nodes')
|
|
||||||
expanded_limit = max(limit * 5, limit)
|
|
||||||
route_options = Query.resolve_offers_by_hub(
|
|
||||||
Query, info, hub_uuid, product_uuid, expanded_limit
|
|
||||||
)
|
|
||||||
offers = []
|
|
||||||
for option in route_options or []:
|
|
||||||
if not option.routes:
|
|
||||||
continue
|
|
||||||
node = nodes_col.get(option.source_uuid)
|
|
||||||
if not node:
|
|
||||||
continue
|
|
||||||
offers.append(OfferWithRouteType(
|
|
||||||
uuid=node['_key'],
|
|
||||||
product_uuid=node.get('product_uuid'),
|
|
||||||
product_name=node.get('product_name'),
|
|
||||||
supplier_uuid=node.get('supplier_uuid'),
|
|
||||||
supplier_name=node.get('supplier_name'),
|
|
||||||
latitude=node.get('latitude'),
|
|
||||||
longitude=node.get('longitude'),
|
|
||||||
country=node.get('country'),
|
|
||||||
country_code=node.get('country_code'),
|
|
||||||
price_per_unit=node.get('price_per_unit'),
|
|
||||||
currency=node.get('currency'),
|
|
||||||
quantity=node.get('quantity'),
|
|
||||||
unit=node.get('unit'),
|
|
||||||
distance_km=option.distance_km,
|
|
||||||
routes=option.routes,
|
|
||||||
))
|
|
||||||
if len(offers) >= limit:
|
|
||||||
break
|
|
||||||
logger.info("Found %d offers by graph for hub %s", len(offers), hub_uuid)
|
|
||||||
return offers
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Error finding offers by hub %s: %s", hub_uuid, e)
|
|
||||||
return []
|
|
||||||
|
|
||||||
aql = """
|
|
||||||
FOR offer IN nodes
|
|
||||||
FILTER offer.node_type == 'offer'
|
|
||||||
FILTER offer.product_uuid != null
|
|
||||||
FILTER offer.latitude != null AND offer.longitude != null
|
|
||||||
"""
|
|
||||||
|
|
||||||
if product_uuid:
|
|
||||||
aql += " FILTER offer.product_uuid == @product_uuid\n"
|
|
||||||
|
|
||||||
aql += """
|
|
||||||
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
|
||||||
FILTER dist <= @radius
|
|
||||||
SORT dist ASC
|
|
||||||
LIMIT @limit
|
|
||||||
RETURN MERGE(offer, {distance_km: dist})
|
|
||||||
"""
|
|
||||||
|
|
||||||
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
|
|
||||||
if product_uuid:
|
|
||||||
bind_vars['product_uuid'] = product_uuid
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor = db.aql.execute(aql, bind_vars=bind_vars)
|
nodes_col = db.collection('nodes')
|
||||||
offer_nodes = list(cursor)
|
|
||||||
logger.info("Found %d offers near (%.3f, %.3f) within %d km", len(offer_nodes), lat, lon, radius)
|
|
||||||
|
|
||||||
# If hub_uuid provided, calculate routes for each offer
|
# If no hub_uuid provided, snap to nearest hub by coordinates.
|
||||||
if hub_uuid:
|
if not hub_uuid:
|
||||||
nodes_col = db.collection('nodes')
|
aql_hub = """
|
||||||
hub = nodes_col.get(hub_uuid)
|
FOR hub IN nodes
|
||||||
if not hub:
|
FILTER hub.node_type == 'logistics' OR hub.node_type == null
|
||||||
logger.warning("Hub %s not found for route calculation", hub_uuid)
|
FILTER hub.product_uuid == null
|
||||||
hub_uuid = None
|
LET types = hub.transport_types != null ? hub.transport_types : []
|
||||||
|
FILTER ('rail' IN types) OR ('sea' IN types)
|
||||||
|
FILTER hub.latitude != null AND hub.longitude != null
|
||||||
|
LET dist = DISTANCE(hub.latitude, hub.longitude, @lat, @lon) / 1000
|
||||||
|
SORT dist ASC
|
||||||
|
LIMIT 1
|
||||||
|
RETURN hub
|
||||||
|
"""
|
||||||
|
cursor = db.aql.execute(aql_hub, bind_vars={'lat': lat, 'lon': lon})
|
||||||
|
hubs = list(cursor)
|
||||||
|
if not hubs:
|
||||||
|
logger.info("No hub found near coordinates (%.3f, %.3f)", lat, lon)
|
||||||
|
return []
|
||||||
|
hub_uuid = hubs[0]['_key']
|
||||||
|
|
||||||
|
expanded_limit = max(limit * 5, limit)
|
||||||
|
route_options = Query.resolve_offers_by_hub(
|
||||||
|
Query, info, hub_uuid, product_uuid, expanded_limit
|
||||||
|
)
|
||||||
offers = []
|
offers = []
|
||||||
for node in offer_nodes:
|
for option in route_options or []:
|
||||||
routes = []
|
if not option.routes:
|
||||||
|
continue
|
||||||
# Calculate route to hub if hub_uuid provided
|
node = nodes_col.get(option.source_uuid)
|
||||||
if hub_uuid:
|
if not node:
|
||||||
route_result = Query.resolve_offer_to_hub(Query, info, node['_key'], hub_uuid)
|
continue
|
||||||
if route_result and route_result.routes:
|
|
||||||
routes = route_result.routes
|
|
||||||
|
|
||||||
offers.append(OfferWithRouteType(
|
offers.append(OfferWithRouteType(
|
||||||
uuid=node['_key'],
|
uuid=node['_key'],
|
||||||
product_uuid=node.get('product_uuid'),
|
product_uuid=node.get('product_uuid'),
|
||||||
@@ -1826,10 +1546,12 @@ class Query(graphene.ObjectType):
|
|||||||
currency=node.get('currency'),
|
currency=node.get('currency'),
|
||||||
quantity=node.get('quantity'),
|
quantity=node.get('quantity'),
|
||||||
unit=node.get('unit'),
|
unit=node.get('unit'),
|
||||||
distance_km=node.get('distance_km'),
|
distance_km=option.distance_km,
|
||||||
routes=routes,
|
routes=option.routes,
|
||||||
))
|
))
|
||||||
|
if len(offers) >= limit:
|
||||||
|
break
|
||||||
|
logger.info("Found %d offers by graph for hub %s", len(offers), hub_uuid)
|
||||||
return offers
|
return offers
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error finding nearest offers: %s", e)
|
logger.error("Error finding nearest offers: %s", e)
|
||||||
@@ -1920,45 +1642,63 @@ class Query(graphene.ObjectType):
|
|||||||
"""Find nearest suppliers to coordinates, optionally filtered by product."""
|
"""Find nearest suppliers to coordinates, optionally filtered by product."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|
||||||
aql = """
|
|
||||||
FOR offer IN nodes
|
|
||||||
FILTER offer.node_type == 'offer'
|
|
||||||
FILTER offer.supplier_uuid != null
|
|
||||||
FILTER offer.latitude != null AND offer.longitude != null
|
|
||||||
"""
|
|
||||||
|
|
||||||
if product_uuid:
|
if product_uuid:
|
||||||
aql += " FILTER offer.product_uuid == @product_uuid\n"
|
aql = """
|
||||||
|
FOR offer IN nodes
|
||||||
|
FILTER offer.node_type == 'offer'
|
||||||
|
FILTER offer.supplier_uuid != null
|
||||||
|
FILTER offer.product_uuid == @product_uuid
|
||||||
|
COLLECT supplier_uuid = offer.supplier_uuid INTO offers
|
||||||
|
LET first_offer = FIRST(offers).offer
|
||||||
|
|
||||||
aql += """
|
// Try to find supplier node for full info
|
||||||
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
LET supplier_node = FIRST(
|
||||||
FILTER dist <= @radius
|
FOR s IN nodes
|
||||||
COLLECT supplier_uuid = offer.supplier_uuid INTO offers
|
FILTER s._key == supplier_uuid
|
||||||
LET first_offer = FIRST(offers).offer
|
FILTER s.node_type == 'supplier'
|
||||||
LET supplier_dist = DISTANCE(first_offer.latitude, first_offer.longitude, @lat, @lon) / 1000
|
RETURN s
|
||||||
|
)
|
||||||
|
|
||||||
// Try to find supplier node for full info
|
LIMIT @limit
|
||||||
LET supplier_node = FIRST(
|
RETURN {
|
||||||
FOR s IN nodes
|
uuid: supplier_uuid,
|
||||||
FILTER s._key == supplier_uuid
|
name: supplier_node != null ? supplier_node.name : first_offer.supplier_name,
|
||||||
FILTER s.node_type == 'supplier'
|
latitude: supplier_node != null ? supplier_node.latitude : first_offer.latitude,
|
||||||
RETURN s
|
longitude: supplier_node != null ? supplier_node.longitude : first_offer.longitude,
|
||||||
)
|
distance_km: null
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
bind_vars = {'product_uuid': product_uuid, 'limit': limit}
|
||||||
|
else:
|
||||||
|
aql = """
|
||||||
|
FOR offer IN nodes
|
||||||
|
FILTER offer.node_type == 'offer'
|
||||||
|
FILTER offer.supplier_uuid != null
|
||||||
|
FILTER offer.latitude != null AND offer.longitude != null
|
||||||
|
LET dist = DISTANCE(offer.latitude, offer.longitude, @lat, @lon) / 1000
|
||||||
|
COLLECT supplier_uuid = offer.supplier_uuid INTO offers
|
||||||
|
LET first_offer = FIRST(offers).offer
|
||||||
|
LET supplier_dist = DISTANCE(first_offer.latitude, first_offer.longitude, @lat, @lon) / 1000
|
||||||
|
|
||||||
SORT supplier_dist ASC
|
// Try to find supplier node for full info
|
||||||
LIMIT @limit
|
LET supplier_node = FIRST(
|
||||||
RETURN {
|
FOR s IN nodes
|
||||||
uuid: supplier_uuid,
|
FILTER s._key == supplier_uuid
|
||||||
name: supplier_node != null ? supplier_node.name : first_offer.supplier_name,
|
FILTER s.node_type == 'supplier'
|
||||||
latitude: supplier_node != null ? supplier_node.latitude : first_offer.latitude,
|
RETURN s
|
||||||
longitude: supplier_node != null ? supplier_node.longitude : first_offer.longitude,
|
)
|
||||||
distance_km: supplier_dist
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
bind_vars = {'lat': lat, 'lon': lon, 'radius': radius, 'limit': limit}
|
SORT supplier_dist ASC
|
||||||
if product_uuid:
|
LIMIT @limit
|
||||||
bind_vars['product_uuid'] = product_uuid
|
RETURN {
|
||||||
|
uuid: supplier_uuid,
|
||||||
|
name: supplier_node != null ? supplier_node.name : first_offer.supplier_name,
|
||||||
|
latitude: supplier_node != null ? supplier_node.latitude : first_offer.latitude,
|
||||||
|
longitude: supplier_node != null ? supplier_node.longitude : first_offer.longitude,
|
||||||
|
distance_km: supplier_dist
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
bind_vars = {'lat': lat, 'lon': lon, 'limit': limit}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cursor = db.aql.execute(aql, bind_vars=bind_vars)
|
cursor = db.aql.execute(aql, bind_vars=bind_vars)
|
||||||
@@ -1971,7 +1711,10 @@ class Query(graphene.ObjectType):
|
|||||||
longitude=s.get('longitude'),
|
longitude=s.get('longitude'),
|
||||||
distance_km=s.get('distance_km'),
|
distance_km=s.get('distance_km'),
|
||||||
))
|
))
|
||||||
logger.info("Found %d suppliers near (%.3f, %.3f) within %d km", len(suppliers), lat, lon, radius)
|
if product_uuid:
|
||||||
|
logger.info("Found %d suppliers for product %s", len(suppliers), product_uuid)
|
||||||
|
else:
|
||||||
|
logger.info("Found %d suppliers near (%.3f, %.3f)", len(suppliers), lat, lon)
|
||||||
return suppliers
|
return suppliers
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error finding nearest suppliers: %s", e)
|
logger.error("Error finding nearest suppliers: %s", e)
|
||||||
|
|||||||
Reference in New Issue
Block a user