diff --git a/src/monitor_utils/database.py b/src/monitor_utils/database.py index 5c84188..246ca34 100644 --- a/src/monitor_utils/database.py +++ b/src/monitor_utils/database.py @@ -7,6 +7,7 @@ alert references for new products and price changes. """ import logging +import math from dataclasses import asdict from datetime import datetime, timezone from functools import cache @@ -318,10 +319,13 @@ def calculate_priority(document: dict) -> int: Always recalculated from current and original price so the priority reflects the prices at alert time rather than any stale stored - percentage. Missing prices, zero original prices, and price increases - resolve to priority 0. A product with no MSRP has no percentage to rank - on and takes NO_MSRP_PRIORITY instead, since a real 0 would leave it - permanently behind every other alert in the claim query's sort. A new + percentage. Missing prices, zero original prices, non-finite prices, a + discount too large to represent, and price increases resolve to priority + 0, so one product's bad pricing can never raise out of the whole cycle (a + retailer field like "Infinity" survives float() and used to crash the + monitor with an OverflowError). A product with no MSRP has no percentage + to rank on and takes NO_MSRP_PRIORITY instead, since a real 0 would leave + it permanently behind every other alert in the claim query's sort. A new listing is floored at NEW_LISTING_PRIORITY for the same reason: it may carry any discount at all, and its percentage rank still wins when higher. @@ -337,10 +341,17 @@ def calculate_priority(document: dict) -> int: if current_price is None or not original_price: return 0 + if not (math.isfinite(current_price) and math.isfinite(original_price)): + return 0 + if not document.get("hasMsrp", True): return NO_MSRP_PRIORITY - percentage = int(((original_price - current_price) / original_price) * 100) + discount = ((original_price - current_price) / original_price) * 100 + if not math.isfinite(discount): + return 0 + + percentage = int(discount) priority = min(max(percentage // 10, 0), 10) if document.get("newListing"): return max(priority, NEW_LISTING_PRIORITY) diff --git a/tests/test_alert_priority.py b/tests/test_alert_priority.py index 19cd01e..968411f 100644 --- a/tests/test_alert_priority.py +++ b/tests/test_alert_priority.py @@ -50,6 +50,23 @@ def test_broken_prices_still_outrank_nothing(): assert calculate_priority(document(None, 100.0, hasMsrp=False)) == 0 +def test_non_finite_prices_resolve_to_zero_instead_of_raising(): + """float("Infinity") from a retailer field once crashed the whole monitor.""" + inf, nan = float("inf"), float("nan") + assert calculate_priority(document(50.0, inf)) == 0 + assert calculate_priority(document(inf, 100.0)) == 0 + assert calculate_priority(document(-inf, 100.0)) == 0 + assert calculate_priority(document(nan, 100.0)) == 0 + assert calculate_priority(document(50.0, nan)) == 0 + assert calculate_priority(document(50.0, inf, newListing=True)) == 0 + + +def test_a_discount_too_large_to_represent_resolves_to_zero(): + """Finite prices can still overflow the ratio (a subnormal original).""" + assert calculate_priority(document(-1e308, 5e-324)) == 0 + assert calculate_priority(document(1e308, 5e-324)) == 0 + + def test_new_listing_is_floored_not_buried(): """A low-discount new listing sends regardless, so it must not sort last.""" assert (