From b510dd54d60062ed88dd8aa6c3b90b8e48516477 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Fri, 16 Jan 2026 01:32:55 +0700 Subject: [PATCH] feat: add catalog navigation queries - findProductsForHub: find products deliverable to a hub - findHubsForProduct: find hubs where product can be delivered - findSupplierProductHubs: find hubs for supplier's product - findOffersForHubByProduct: find offers with routes (wrapper for findProductRoutes) --- geo_app/schema.py | 202 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/geo_app/schema.py b/geo_app/schema.py index 9a5eff4..590e142 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -183,6 +183,34 @@ class Query(graphene.ObjectType): description="Get clustered nodes for map display (server-side clustering)", ) + # Business-oriented queries for catalog navigation + find_products_for_hub = graphene.List( + graphene.String, + hub_uuid=graphene.String(required=True), + description="Find unique product UUIDs that can be delivered to this hub", + ) + + find_hubs_for_product = graphene.List( + NodeType, + product_uuid=graphene.String(required=True), + description="Find logistics hubs where this product can be delivered", + ) + + find_supplier_product_hubs = graphene.List( + NodeType, + supplier_uuid=graphene.String(required=True), + product_uuid=graphene.String(required=True), + description="Find hubs where this supplier can deliver this product", + ) + + find_offers_for_hub_by_product = graphene.List( + ProductRouteOptionType, + hub_uuid=graphene.String(required=True), + product_uuid=graphene.String(required=True), + limit_sources=graphene.Int(default_value=10), + description="Find product offers that can be delivered to hub (with routes)", + ) + @staticmethod def _build_routes(db, from_uuid, to_uuid, limit): """Shared helper to compute K shortest routes between two nodes.""" @@ -768,6 +796,180 @@ class Query(graphene.ObjectType): clusters = get_clustered_nodes(db, west, south, east, north, zoom, transport_type) return [ClusterPointType(**c) for c in clusters] + def resolve_find_products_for_hub(self, info, hub_uuid): + """ + Find unique product UUIDs that can be delivered to this hub. + Uses reverse traversal from hub to find reachable offer nodes. + """ + db = get_db() + ensure_graph() + + # Check hub exists + nodes_col = db.collection('nodes') + hub = nodes_col.get(hub_uuid) + if not hub: + logger.info("Hub %s not found", hub_uuid) + return [] + + # Find all offer nodes reachable from hub via graph traversal + # Offer nodes have product_uuid field + aql = """ + FOR v, e, p IN 1..10 ANY @hub_id GRAPH 'optovia_graph' + FILTER v.product_uuid != null + COLLECT product_uuid = v.product_uuid + RETURN product_uuid + """ + try: + cursor = db.aql.execute( + aql, + bind_vars={'hub_id': f'nodes/{hub_uuid}'}, + ) + product_uuids = list(cursor) + logger.info("Found %d products for hub %s", len(product_uuids), hub_uuid) + return product_uuids + except Exception as e: + logger.error("Error finding products for hub: %s", e) + return [] + + def resolve_find_hubs_for_product(self, info, product_uuid): + """ + Find logistics hubs where this product can be delivered. + Finds offer nodes with this product, then finds reachable hubs. + """ + db = get_db() + ensure_graph() + + # Find all offer nodes with this product_uuid + aql_offers = """ + FOR node IN nodes + FILTER node.product_uuid == @product_uuid + RETURN node._key + """ + try: + cursor = db.aql.execute(aql_offers, bind_vars={'product_uuid': product_uuid}) + offer_keys = list(cursor) + except Exception as e: + logger.error("Error finding offers for product: %s", e) + return [] + + if not offer_keys: + logger.info("No offers found for product %s", product_uuid) + return [] + + # Find hubs reachable from these offer nodes + aql_hubs = """ + LET offer_ids = @offer_ids + FOR offer_id IN offer_ids + FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph' + FILTER (v.node_type == 'logistics' OR v.node_type == null) + FILTER v.product_uuid == null + COLLECT hub_key = v._key INTO hubs + LET hub = FIRST(hubs).v + RETURN hub + """ + try: + cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys}) + hubs = list(cursor) + + result = [] + for hub in hubs: + if hub: + result.append(NodeType( + uuid=hub.get('_key'), + name=hub.get('name'), + latitude=hub.get('latitude'), + longitude=hub.get('longitude'), + country=hub.get('country'), + country_code=hub.get('country_code'), + synced_at=hub.get('synced_at'), + transport_types=hub.get('transport_types') or [], + edges=[], + )) + + logger.info("Found %d hubs for product %s", len(result), product_uuid) + return result + except Exception as e: + logger.error("Error finding hubs for product: %s", e) + return [] + + def resolve_find_supplier_product_hubs(self, info, supplier_uuid, product_uuid): + """ + Find hubs where this supplier can deliver this product. + Finds offer nodes matching both supplier and product, then finds reachable hubs. + """ + db = get_db() + ensure_graph() + + # Find offer nodes with this supplier_uuid AND product_uuid + aql_offers = """ + FOR node IN nodes + FILTER node.product_uuid == @product_uuid + FILTER node.supplier_uuid == @supplier_uuid + RETURN node._key + """ + try: + cursor = db.aql.execute( + aql_offers, + bind_vars={'product_uuid': product_uuid, 'supplier_uuid': supplier_uuid} + ) + offer_keys = list(cursor) + except Exception as e: + logger.error("Error finding supplier offers: %s", e) + return [] + + if not offer_keys: + logger.info("No offers found for supplier %s, product %s", supplier_uuid, product_uuid) + return [] + + # Find hubs reachable from these offer nodes + aql_hubs = """ + LET offer_ids = @offer_ids + FOR offer_id IN offer_ids + FOR v, e, p IN 1..10 ANY CONCAT('nodes/', offer_id) GRAPH 'optovia_graph' + FILTER (v.node_type == 'logistics' OR v.node_type == null) + FILTER v.product_uuid == null + COLLECT hub_key = v._key INTO hubs + LET hub = FIRST(hubs).v + RETURN hub + """ + try: + cursor = db.aql.execute(aql_hubs, bind_vars={'offer_ids': offer_keys}) + hubs = list(cursor) + + result = [] + for hub in hubs: + if hub: + result.append(NodeType( + uuid=hub.get('_key'), + name=hub.get('name'), + latitude=hub.get('latitude'), + longitude=hub.get('longitude'), + country=hub.get('country'), + country_code=hub.get('country_code'), + synced_at=hub.get('synced_at'), + transport_types=hub.get('transport_types') or [], + edges=[], + )) + + logger.info("Found %d hubs for supplier %s product %s", len(result), supplier_uuid, product_uuid) + return result + except Exception as e: + logger.error("Error finding supplier product hubs: %s", e) + return [] + + def resolve_find_offers_for_hub_by_product(self, info, hub_uuid, product_uuid, limit_sources=10): + """ + Find product offers that can be delivered to hub (with routes). + Same as find_product_routes but with business-oriented naming. + """ + return self.resolve_find_product_routes( + info, + product_uuid=product_uuid, + to_uuid=hub_uuid, + limit_sources=limit_sources, + limit_routes=1, + ) + schema = graphene.Schema(query=Query)