From f5f261ff899c411b39474460119219db526b68fb Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:57:24 +0700 Subject: [PATCH] Add quote calculations query --- geo_app/schema.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/geo_app/schema.py b/geo_app/schema.py index e4271c4..1611b78 100644 --- a/geo_app/schema.py +++ b/geo_app/schema.py @@ -161,6 +161,11 @@ class OfferWithRouteType(graphene.ObjectType): routes = graphene.List(lambda: RoutePathType) +class OfferCalculationType(graphene.ObjectType): + """Calculation result that may include one or multiple offers.""" + offers = graphene.List(lambda: OfferWithRouteType) + + class Query(graphene.ObjectType): """Root query.""" MAX_EXPANSIONS = 20000 @@ -341,6 +346,18 @@ class Query(graphene.ObjectType): description="Find nearest offers to coordinates with optional routes to hub", ) + quote_calculations = graphene.List( + OfferCalculationType, + lat=graphene.Float(required=True, description="Latitude"), + lon=graphene.Float(required=True, description="Longitude"), + radius=graphene.Float(default_value=500, description="Search radius in km"), + product_uuid=graphene.String(description="Filter by product UUID"), + hub_uuid=graphene.String(description="Hub UUID - if provided, calculates routes to this hub"), + quantity=graphene.Float(description="Requested quantity to cover (optional)"), + limit=graphene.Int(default_value=10, description="Max calculations"), + description="Get quote calculations (single offer or split offers)", + ) + nearest_suppliers = graphene.List( SupplierType, lat=graphene.Float(required=True, description="Latitude"), @@ -1734,6 +1751,87 @@ class Query(graphene.ObjectType): logger.error("Error finding nearest offers: %s", e) return [] + def resolve_quote_calculations(self, info, lat, lon, radius=500, product_uuid=None, hub_uuid=None, quantity=None, limit=10): + """Return calculations as arrays of offers. If quantity provided, may return split offers.""" + def _parse_number(value): + if value is None: + return None + try: + return float(str(value).replace(',', '.')) + except (TypeError, ValueError): + return None + + def _offer_quantity(offer): + return _parse_number(getattr(offer, 'quantity', None)) + + def _offer_price(offer): + return _parse_number(getattr(offer, 'price_per_unit', None)) + + def _offer_distance(offer): + if getattr(offer, 'distance_km', None) is not None: + return offer.distance_km or 0 + if getattr(offer, 'routes', None): + route = offer.routes[0] if offer.routes else None + if route and route.total_distance_km is not None: + return route.total_distance_km or 0 + return 0 + + offers = self.resolve_nearest_offers( + info, + lat=lat, + lon=lon, + radius=radius, + product_uuid=product_uuid, + hub_uuid=hub_uuid, + limit=max(limit * 4, limit), + ) + + if not offers: + return [] + + quantity_value = _parse_number(quantity) + offers_sorted = sorted( + offers, + key=lambda o: ( + _offer_price(o) if _offer_price(o) is not None else float('inf'), + _offer_distance(o), + ), + ) + + calculations = [] + + # If no requested quantity, return single-offer calculations. + if not quantity_value or quantity_value <= 0: + return [OfferCalculationType(offers=[offer]) for offer in offers_sorted[:limit]] + + # Try single offer that covers quantity. + single = next( + (offer for offer in offers_sorted if (_offer_quantity(offer) or 0) >= quantity_value), + None, + ) + if single: + calculations.append(OfferCalculationType(offers=[single])) + + # Build a split offer (two offers) if possible. + if len(offers_sorted) >= 2: + primary = offers_sorted[0] + remaining = quantity_value - (_offer_quantity(primary) or 0) + if remaining <= 0: + secondary = offers_sorted[1] + else: + secondary = next( + (offer for offer in offers_sorted[1:] if (_offer_quantity(offer) or 0) >= remaining), + offers_sorted[1], + ) + if secondary: + calculations.append(OfferCalculationType(offers=[primary, secondary])) + + # Fallback: ensure at least one calculation. + if not calculations: + calculations.append(OfferCalculationType(offers=[offers_sorted[0]])) + + return calculations[:limit] + def resolve_nearest_suppliers(self, info, lat, lon, radius=1000, product_uuid=None, limit=12): """Find nearest suppliers to coordinates, optionally filtered by product.""" db = get_db()