Files
monitor-shared-utils/tests/test_build_product_details.py
T
dsqlandClaude Code Opus 5.5 6ea3187b33 build_product_details: skip non-finite prices instead of raising OverflowError
PR #25 guarded calculate_priority but missed the same calculation here.
On 2026-10-09 06:25:50Z the consumer hit a product with an infinite price;
runner.py:201

    percentage = int(((original_price - current_price) / original_price) * 100)

raised `OverflowError: cannot convert float infinity to integer`. The
Discord leg dropped that one alert, but the API leg's caller does not
catch OverflowError, so the exception escaped and the consumer logged
CRITICAL "api deal loop stopped" -- no deals reached the API from then on
(nothing sent at all after 22:00Z), while the bot itself stayed "alive".

build_product_details now returns None (with a warning, like the
existing missing-price skip) when either price is a non-finite float, on
BOTH paths: the MSRP-less path never computes a percentage and would
otherwise carry inf into the Product and the API payload. A finite pair
whose ratio still overflows (a subnormal original price) is skipped too.
Ints and other types keep their existing behaviour.

Tests (tests/test_build_product_details.py): five new cases fail on the
old code -- OverflowError, ValueError for nan, and the MSRP-less case
building a Product with inf -- and all 25 pass now. Ruff findings on the
two files are unchanged from main (5, all pre-existing).

Rebuild every service that installs monitor-utils with --no-cache to pick
this up (it is pulled unpinned from main).

Co-Authored-By: Claude Code Opus 5.5 (ansible-ops, auto-repair) <noreply@anthropic.com>
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-10-09 23:22:36 -04:00

244 lines
8.4 KiB
Python

"""unit test: build_product_details thresholding, promo read, and combined_discount
run from the repo root:
uv run pytest tests/test_build_product_details.py
pure and offline — no Mongo. Builds a Product from a raw doc dict and checks the
discount threshold (with percentage codes compounding onto the sale price), the
promo fields read through via .get, and the combined_discount helper.
"""
from datetime import datetime, timezone
from monitor_utils.runner import build_product_details, combined_discount
def _doc(**overrides) -> dict:
"""A well-formed product doc; override prices/promo per test."""
doc = {
"_id": "p1_tools",
"title": "Widget",
"url": "http://x/p1",
"currentPrice": 80.0,
"originalPrice": 100.0,
"imageUrl": "http://img/1",
"customizationIndex": None,
"memberNonMemberComparison": None,
"overstockText": None,
"discoveredDate": datetime(2026, 7, 26, tzinfo=timezone.utc),
"startDate": None,
"endDate": None,
"priceBeforeDrop": None,
"eduPrice": None,
"pastPrices": [
{"price": 80.0, "date": datetime(2026, 7, 26, tzinfo=timezone.utc)}
],
"available": True,
"condition": None,
"appExclusive": None,
"memberPick": None,
"memberDeal": None,
}
doc.update(overrides)
return doc
# ------------------------------------------------------------ combined_discount
def test_combined_discount_no_promo_is_base():
assert combined_discount(24, None) == 24
assert combined_discount(24, 0) == 24
def test_combined_discount_compounds_not_adds():
# 20% off then extra 30% code -> 44% off list (customer pays $56), not 50%
assert combined_discount(20, 30) == 44
def test_combined_discount_rounds_to_nearest():
# 24% sale + 22% code: 1 - .76*.78 = .4072 -> 41 (rounded, not truncated 40)
assert combined_discount(24, 22) == 41
# --------------------------------------------------------- build_product_details
def test_promo_fields_read_through():
doc = _doc(
promoCode="BIRTHDAY26",
promoPercentage=22,
promoDollarOff=None,
promoMinSpend=None,
)
product = build_product_details(doc, min_percentage=0)
assert product.metadata.promo_code == "BIRTHDAY26"
assert product.metadata.promo_percentage == 22
# base percentage is the raw price delta, unchanged by the code
assert product.metadata.percentage == 20
def test_asin_reads_through():
"""asin round-trips: convert_product_to_mongo writes it, so the consumer
must read it back for the SellerAmp lookup"""
product = build_product_details(_doc(asin="B0CHX3QBCH"), min_percentage=0)
assert product.metadata.asin == "B0CHX3QBCH"
def test_missing_asin_defaults_to_none():
assert build_product_details(_doc(), min_percentage=0).metadata.asin is None
def test_missing_promo_keys_default_to_none():
"""a doc from a lagging monitor lacks promo keys -> None, not KeyError"""
product = build_product_details(_doc(), min_percentage=0)
assert product.metadata.promo_code is None
assert product.metadata.promo_percentage is None
def test_percentage_code_lifts_item_over_threshold():
# 30% base fails min 40, but +22% code -> 45% combined clears it
doc = _doc(currentPrice=70.0, originalPrice=100.0, promoPercentage=22)
assert build_product_details(doc, min_percentage=40) is not None
def test_below_threshold_without_code_is_skipped():
doc = _doc(currentPrice=70.0, originalPrice=100.0) # 30%, no code
assert build_product_details(doc, min_percentage=40) is None
def test_dollar_code_does_not_affect_threshold():
# a dollar code has no promoPercentage, so 30% base stays below min 40
doc = _doc(
currentPrice=70.0,
originalPrice=100.0,
promoCode="STORE22",
promoDollarOff=22,
promoMinSpend=100,
)
assert build_product_details(doc, min_percentage=40) is None
# ----------------------------------------------------------------- pastPrices
def test_past_prices_read_through():
"""Price history passes through unchanged."""
history = [
{"price": 95.0, "date": datetime(2026, 7, 20, tzinfo=timezone.utc)},
{"price": 90.0, "date": datetime(2026, 7, 24, tzinfo=timezone.utc)},
]
product = build_product_details(_doc(pastPrices=history), min_percentage=0)
assert product.pricing.past_prices == history
# ----------------------------------------------------------- bypass_thresholds
def test_bypass_builds_a_below_threshold_product():
"""A new listing sends at any discount when the caller says so."""
doc = _doc(currentPrice=95.0, originalPrice=100.0) # 5%, way under 40
product = build_product_details(doc, min_percentage=40, bypass_thresholds=True)
assert product is not None
assert product.metadata.percentage == 5
def test_bypass_defaults_off():
doc = _doc(currentPrice=95.0, originalPrice=100.0)
assert build_product_details(doc, min_percentage=40) is None
def test_bypass_does_not_rescue_broken_prices():
"""The missing-price skip is a data guard, not a threshold."""
doc = _doc(currentPrice=95.0, originalPrice=None)
assert build_product_details(doc, min_percentage=40, bypass_thresholds=True) is None
# --------------------------------------------------------------- newListing
def test_new_listing_flag_reads_through():
product = build_product_details(_doc(newListing=True), min_percentage=0)
assert product.metadata.new_listing is True
def test_legacy_docs_without_the_flag_build_as_none():
"""Documents written before newListing existed must not KeyError."""
product = build_product_details(_doc(), min_percentage=0)
assert product.metadata.new_listing is None
# ------------------------------------------------------------ msrp-less guard
def test_msrpless_doc_with_missing_original_price_skips_not_crashes():
"""Regression: the guard lived inside the has_msrp branch, so this doc
shape raised ValidationError (not an expected error) and killed the
consumer."""
doc = _doc(hasMsrp=False, originalPrice=None)
assert build_product_details(doc, min_percentage=40) is None
def test_msrpless_doc_with_zero_original_price_skips():
doc = _doc(hasMsrp=False, originalPrice=0)
assert build_product_details(doc, min_percentage=40) is None
def test_bypass_does_not_rescue_msrpless_broken_prices():
doc = _doc(hasMsrp=False, originalPrice=None)
assert build_product_details(doc, min_percentage=40, bypass_thresholds=True) is None
def test_msrpless_doc_with_prices_builds_unthresholded():
"""A priced MSRP-less doc still skips the threshold gate entirely."""
doc = _doc(hasMsrp=False, currentPrice=95.0, originalPrice=100.0) # 5% < 40
product = build_product_details(doc, min_percentage=40)
assert product is not None
assert product.pricing.has_msrp is False
# ------------------------------------------------------------ non-finite prices
def test_infinite_price_skips_instead_of_raising():
"""Regression (2026-10-09 06:25Z): float("Infinity") from a retailer field
raised OverflowError here and stopped the consumer's API deal loop."""
inf = float("inf")
assert build_product_details(_doc(originalPrice=inf), min_percentage=40) is None
assert build_product_details(_doc(currentPrice=inf), min_percentage=40) is None
assert build_product_details(_doc(currentPrice=-inf), min_percentage=40) is None
def test_nan_price_skips_instead_of_raising():
nan = float("nan")
assert build_product_details(_doc(currentPrice=nan), min_percentage=40) is None
assert build_product_details(_doc(originalPrice=nan), min_percentage=40) is None
def test_msrpless_infinite_price_skips_rather_than_reaching_the_payload():
"""Without the skip the MSRP-less path would carry inf into the Product."""
doc = _doc(hasMsrp=False, currentPrice=float("inf"))
assert build_product_details(doc, min_percentage=40) is None
def test_bypass_does_not_rescue_a_non_finite_price():
doc = _doc(originalPrice=float("inf"))
assert build_product_details(doc, min_percentage=40, bypass_thresholds=True) is None
def test_unrepresentable_discount_skips_instead_of_raising():
"""Finite prices can still overflow the ratio (a subnormal original)."""
doc = _doc(currentPrice=-1e308, originalPrice=5e-324)
assert build_product_details(doc, min_percentage=40) is None