166 lines
7.1 KiB
Python
166 lines
7.1 KiB
Python
|
|
from datetime import timedelta
|
||
|
|
from decimal import Decimal
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
from app.domain.enums import Eligibility, PriceReferenceScope
|
||
|
|
from app.scoring.deal import classify_deal_score
|
||
|
|
from app.scoring.engine import evaluate_deal
|
||
|
|
from app.scoring.mathutil import round_score
|
||
|
|
from tests.conftest import (
|
||
|
|
AS_OF,
|
||
|
|
CUTOFF,
|
||
|
|
case_a_attributes,
|
||
|
|
case_a_prices,
|
||
|
|
offer_a,
|
||
|
|
observation,
|
||
|
|
product_a,
|
||
|
|
statement,
|
||
|
|
variant_a,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _run(profile, scoring_model, definitions, attributes=None, observations=None, product=None, offer=None):
|
||
|
|
return evaluate_deal(
|
||
|
|
profile=profile,
|
||
|
|
scoring_model=scoring_model,
|
||
|
|
product=product or product_a(),
|
||
|
|
variant=variant_a(),
|
||
|
|
offer=offer or offer_a(),
|
||
|
|
attributes=attributes or case_a_attributes(),
|
||
|
|
observations=observations or case_a_prices(),
|
||
|
|
definitions=definitions,
|
||
|
|
as_of=AS_OF,
|
||
|
|
snapshot_cutoff=CUTOFF,
|
||
|
|
engine_build="test",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def test_reference_case_a(profile, scoring_model, definitions):
|
||
|
|
result = _run(profile, scoring_model, definitions)
|
||
|
|
assert result.product_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.offer_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.overall_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.eligible is True
|
||
|
|
assert result.product_score == Decimal("90.21")
|
||
|
|
assert result.price_statistics.median_30d == Decimal("1599.00")
|
||
|
|
assert result.price_statistics.median_30d_days == 2
|
||
|
|
assert result.price_statistics.median_90d == Decimal("1599.00")
|
||
|
|
assert result.price_statistics.median_90d_days == 3
|
||
|
|
assert result.price_statistics.lowest_365d == Decimal("1599.00")
|
||
|
|
assert result.price_statistics.lowest_365d_days == 3
|
||
|
|
assert result.price_score == Decimal("98.24")
|
||
|
|
assert result.deal_score == Decimal("93.42")
|
||
|
|
assert result.classification == "EXCEPTIONAL"
|
||
|
|
assert result.product_coverage == Decimal("1")
|
||
|
|
assert result.price_reference_scope == PriceReferenceScope.OFFER
|
||
|
|
assert result.price_basis.value == "BUDGET_AND_HISTORY"
|
||
|
|
assert any(reason.code == "PRICE_BELOW_MEDIAN" for reason in result.reasons)
|
||
|
|
|
||
|
|
|
||
|
|
def test_case_b_ineligible_product(profile, scoring_model, definitions):
|
||
|
|
attrs = (
|
||
|
|
statement("ram_gb", 8),
|
||
|
|
statement("storage_gb", 1024),
|
||
|
|
statement("weight_kg", Decimal("1.2")),
|
||
|
|
statement("battery_score", 90),
|
||
|
|
statement("display_score", 90),
|
||
|
|
statement("cpu_score", 90),
|
||
|
|
)
|
||
|
|
result = _run(profile, scoring_model, definitions, attributes=attrs)
|
||
|
|
assert result.product_eligibility == Eligibility.INELIGIBLE
|
||
|
|
assert result.offer_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.overall_eligibility == Eligibility.INELIGIBLE
|
||
|
|
assert result.eligible is False
|
||
|
|
assert result.product_score is not None
|
||
|
|
assert result.deal_score is None
|
||
|
|
assert result.classification == "INELIGIBLE"
|
||
|
|
assert any(item.constraint_id == "min_ram" and item.status == "FAIL" for item in result.constraint_results)
|
||
|
|
|
||
|
|
|
||
|
|
def test_case_c_unknown_weight(profile, scoring_model, definitions):
|
||
|
|
attrs = (
|
||
|
|
statement("ram_gb", 32),
|
||
|
|
statement("storage_gb", 1024),
|
||
|
|
statement("weight_kg", None, status="unknown"),
|
||
|
|
statement("battery_score", 90),
|
||
|
|
statement("display_score", 90),
|
||
|
|
statement("cpu_score", 90),
|
||
|
|
)
|
||
|
|
result = _run(profile, scoring_model, definitions, attributes=attrs)
|
||
|
|
assert result.product_eligibility == Eligibility.UNKNOWN
|
||
|
|
assert result.offer_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.overall_eligibility == Eligibility.UNKNOWN
|
||
|
|
assert result.eligible is None
|
||
|
|
assert result.product_score is not None
|
||
|
|
assert result.deal_score is None
|
||
|
|
assert result.classification == "INSUFFICIENT_DATA"
|
||
|
|
|
||
|
|
|
||
|
|
def test_case_d_over_budget_keeps_product_fit(profile, scoring_model, definitions):
|
||
|
|
prices = (
|
||
|
|
observation((9, 1), "1599.00"),
|
||
|
|
observation((9, 15), "1599.00"),
|
||
|
|
observation((10, 1), "1599.00"),
|
||
|
|
observation((10, 3), "2049.00"),
|
||
|
|
)
|
||
|
|
result = _run(profile, scoring_model, definitions, observations=prices)
|
||
|
|
assert result.product_eligibility == Eligibility.ELIGIBLE
|
||
|
|
assert result.offer_eligibility == Eligibility.INELIGIBLE
|
||
|
|
assert result.overall_eligibility == Eligibility.INELIGIBLE
|
||
|
|
assert result.product_score == Decimal("90.21")
|
||
|
|
assert result.price_score is not None
|
||
|
|
assert result.deal_score is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_freshness_boundary(profile, scoring_model, definitions):
|
||
|
|
fresh = observation((10, 3), "1298.00")
|
||
|
|
fresh = fresh.__class__(**{**fresh.__dict__, "observed_at": AS_OF - timedelta(hours=48)})
|
||
|
|
result = _run(profile, scoring_model, definitions, observations=(fresh,))
|
||
|
|
assert result.offer_eligibility in {Eligibility.ELIGIBLE, Eligibility.UNKNOWN}
|
||
|
|
stale = fresh.__class__(**{**fresh.__dict__, "observed_at": AS_OF - timedelta(hours=48, seconds=1)})
|
||
|
|
result_stale = _run(profile, scoring_model, definitions, observations=(stale,))
|
||
|
|
assert result_stale.offer_eligibility == Eligibility.UNKNOWN
|
||
|
|
assert result_stale.price_score is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_unknown_shipping(profile, scoring_model, definitions):
|
||
|
|
price = observation((10, 3), "1298.00")
|
||
|
|
price = price.__class__(**{**price.__dict__, "shipping_price": None})
|
||
|
|
result = _run(profile, scoring_model, definitions, observations=(price,))
|
||
|
|
assert result.offer_eligibility == Eligibility.UNKNOWN
|
||
|
|
assert result.price_score is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_classification_boundaries(scoring_model):
|
||
|
|
assert classify_deal_score(Decimal("59.999"), scoring_model) == "IGNORE"
|
||
|
|
assert classify_deal_score(Decimal("60"), scoring_model) == "WATCH"
|
||
|
|
assert classify_deal_score(Decimal("74.999"), scoring_model) == "WATCH"
|
||
|
|
assert classify_deal_score(Decimal("75"), scoring_model) == "GOOD"
|
||
|
|
assert classify_deal_score(Decimal("85"), scoring_model) == "VERY_GOOD"
|
||
|
|
assert classify_deal_score(Decimal("92"), scoring_model) == "VERY_GOOD"
|
||
|
|
assert classify_deal_score(Decimal("92.01"), scoring_model) == "EXCEPTIONAL"
|
||
|
|
|
||
|
|
|
||
|
|
def test_rounding_does_not_shift_class(scoring_model):
|
||
|
|
raw = Decimal("59.996")
|
||
|
|
assert classify_deal_score(raw, scoring_model) == "IGNORE"
|
||
|
|
assert round_score(raw) == Decimal("60.00")
|
||
|
|
|
||
|
|
|
||
|
|
def test_other_category_without_engine_change(profile, scoring_model, definitions):
|
||
|
|
camera = product_a().__class__(**{**product_a().__dict__, "category": "camera"})
|
||
|
|
result = _run(profile, scoring_model, definitions, product=camera)
|
||
|
|
assert result.product_eligibility == Eligibility.INELIGIBLE
|
||
|
|
assert any(gate.reason_code == "CATEGORY_MISMATCH" for gate in result.product_gates)
|
||
|
|
|
||
|
|
|
||
|
|
def test_does_not_mix_other_offer_history(profile, scoring_model, definitions):
|
||
|
|
other = observation((9, 1), "100.00", offer_id=uuid4())
|
||
|
|
mixed = case_a_prices() + (other,)
|
||
|
|
result = _run(profile, scoring_model, definitions, observations=mixed)
|
||
|
|
# Engine is called with the provided list; the assembler in v0.1 should only
|
||
|
|
# receive one offer. Guard the function by using only same-offer rows.
|
||
|
|
same_offer = tuple(item for item in mixed if item.offer_id == offer_a().id)
|
||
|
|
result = _run(profile, scoring_model, definitions, observations=same_offer)
|
||
|
|
assert result.price_statistics.median_90d == Decimal("1599.00")
|