Add quote calculations query
All checks were successful
Build Docker Image / build (push) Successful in 1m53s
All checks were successful
Build Docker Image / build (push) Successful in 1m53s
This commit is contained in:
@@ -161,6 +161,11 @@ class OfferWithRouteType(graphene.ObjectType):
|
|||||||
routes = graphene.List(lambda: RoutePathType)
|
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):
|
class Query(graphene.ObjectType):
|
||||||
"""Root query."""
|
"""Root query."""
|
||||||
MAX_EXPANSIONS = 20000
|
MAX_EXPANSIONS = 20000
|
||||||
@@ -341,6 +346,18 @@ class Query(graphene.ObjectType):
|
|||||||
description="Find nearest offers to coordinates with optional routes to hub",
|
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(
|
nearest_suppliers = graphene.List(
|
||||||
SupplierType,
|
SupplierType,
|
||||||
lat=graphene.Float(required=True, description="Latitude"),
|
lat=graphene.Float(required=True, description="Latitude"),
|
||||||
@@ -1734,6 +1751,87 @@ class Query(graphene.ObjectType):
|
|||||||
logger.error("Error finding nearest offers: %s", e)
|
logger.error("Error finding nearest offers: %s", e)
|
||||||
return []
|
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):
|
def resolve_nearest_suppliers(self, info, lat, lon, radius=1000, product_uuid=None, limit=12):
|
||||||
"""Find nearest suppliers to coordinates, optionally filtered by product."""
|
"""Find nearest suppliers to coordinates, optionally filtered by product."""
|
||||||
db = get_db()
|
db = get_db()
|
||||||
|
|||||||
Reference in New Issue
Block a user