added combined discount for consumer so codes can be calculated on the fly for any monitor
This commit is contained in:
1 parent
e9632470fd
commit
4c1033057a
3 files changed
+141
-2
No files matched your search
@@ -347,6 +347,11 @@ def convert_product_to_mongo(product: Product) -> dict:
|
|||||||
"customizationIndex": metadata["customization_index"],
|
"customizationIndex": metadata["customization_index"],
|
||||||
"memberNonMemberComparison": metadata["member_non_member_comparison"],
|
"memberNonMemberComparison": metadata["member_non_member_comparison"],
|
||||||
"overstockText": metadata["overstock_text"],
|
"overstockText": metadata["overstock_text"],
|
||||||
|
"affiliateUrl": metadata["affiliate_url"],
|
||||||
|
"promoCode": metadata["promo_code"],
|
||||||
|
"promoPercentage": metadata["promo_percentage"],
|
||||||
|
"promoDollarOff": metadata["promo_dollar_off"],
|
||||||
|
"promoMinSpend": metadata["promo_min_spend"],
|
||||||
"startDate": timing["start_date"],
|
"startDate": timing["start_date"],
|
||||||
"endDate": timing["end_date"],
|
"endDate": timing["end_date"],
|
||||||
"priceBeforeDrop": pricing["price_before_drop"],
|
"priceBeforeDrop": pricing["price_before_drop"],
|
||||||
|
|||||||
@@ -116,6 +116,27 @@ async def setup_config(
|
|||||||
return category_names, config["channels"], collection
|
return category_names, config["channels"], collection
|
||||||
|
|
||||||
|
|
||||||
|
def combined_discount(base_percentage: int, promo_percentage: int | None) -> int:
|
||||||
|
"""Combine an on-sale discount with a percentage code applied at checkout.
|
||||||
|
|
||||||
|
A "extra N% off" code discounts the already-reduced sale price, so the two
|
||||||
|
compound rather than add: 20% off then an extra 30% code is 44% off list,
|
||||||
|
not 50%. Returns base_percentage unchanged when there is no percentage code.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_percentage: The on-sale discount off list, as a whole percent.
|
||||||
|
promo_percentage: The code's advertised percent off, or None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The combined discount off list, rounded to a whole percent.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not promo_percentage:
|
||||||
|
return base_percentage
|
||||||
|
|
||||||
|
return round(100 - (100 - base_percentage) * (100 - promo_percentage) / 100)
|
||||||
|
|
||||||
|
|
||||||
def build_product_details(
|
def build_product_details(
|
||||||
product: dict,
|
product: dict,
|
||||||
min_percentage: int,
|
min_percentage: int,
|
||||||
@@ -148,7 +169,11 @@ def build_product_details(
|
|||||||
|
|
||||||
percentage = int(((original_price - current_price) / original_price) * 100)
|
percentage = int(((original_price - current_price) / original_price) * 100)
|
||||||
price_drop = original_price - current_price
|
price_drop = original_price - current_price
|
||||||
passes_percentage = percentage >= min_percentage
|
# A percentage code compounds onto the sale price, so an item can clear the
|
||||||
|
# threshold on the combined discount even when the base discount would not.
|
||||||
|
# Dollar codes do not map to a percent and never affect the threshold.
|
||||||
|
effective_percentage = combined_discount(percentage, product.get("promoPercentage"))
|
||||||
|
passes_percentage = effective_percentage >= min_percentage
|
||||||
passes_price_drop = min_price_drop is not None and price_drop >= min_price_drop
|
passes_price_drop = min_price_drop is not None and price_drop >= min_price_drop
|
||||||
if not passes_percentage and not passes_price_drop:
|
if not passes_percentage and not passes_price_drop:
|
||||||
return
|
return
|
||||||
@@ -164,6 +189,10 @@ def build_product_details(
|
|||||||
overstock_text=product["overstockText"],
|
overstock_text=product["overstockText"],
|
||||||
percentage=percentage,
|
percentage=percentage,
|
||||||
affiliate_url=product.get("affiliateUrl"),
|
affiliate_url=product.get("affiliateUrl"),
|
||||||
|
promo_code=product.get("promoCode"),
|
||||||
|
promo_percentage=product.get("promoPercentage"),
|
||||||
|
promo_dollar_off=product.get("promoDollarOff"),
|
||||||
|
promo_min_spend=product.get("promoMinSpend"),
|
||||||
),
|
),
|
||||||
timing=ProductTiming(
|
timing=ProductTiming(
|
||||||
discovered_date=product["discoveredDate"],
|
discovered_date=product["discoveredDate"],
|
||||||
@@ -351,7 +380,7 @@ async def execute_pagination(
|
|||||||
|
|
||||||
async def fetch_page(page_index: int):
|
async def fetch_page(page_index: int):
|
||||||
"""Fetch a single page or offset, optionally under the shared semaphore."""
|
"""Fetch a single page or offset, optionally under the shared semaphore."""
|
||||||
async with (semaphore or nullcontext()):
|
async with semaphore or nullcontext():
|
||||||
if stop_event.is_set():
|
if stop_event.is_set():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"""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,
|
||||||
|
"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_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
|
||||||
Reference in new issue
Block a user