70 lines
2.9 KiB
Python
70 lines
2.9 KiB
Python
"""Tests for the deterministic pricing gate (playbook Definition of Done, §13)."""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from config.settings import PricingRules
|
|
from pricing_agent.schemas import BuyBoxStatus, CompetitiveSnapshot, Decision
|
|
from pricing_agent.tools.gate import evaluate_gate
|
|
from pricing_agent.tools.margin_engine import worst_case_stack
|
|
|
|
RULES = PricingRules(
|
|
margin_floor=0.25,
|
|
returns_reserve_pct=0.02,
|
|
storage_alloc=0.25,
|
|
worst_case_referral_pct=0.15,
|
|
)
|
|
|
|
|
|
def _stack(price):
|
|
return worst_case_stack(selling_price=price, landed_cost=8.00, fba_fee=5.50, rules=RULES)
|
|
|
|
|
|
def test_healthy_sku_won_buybox_is_approved():
|
|
comp = CompetitiveSnapshot(buy_box_status=BuyBoxStatus.WON, competitive_median=24.99)
|
|
decision, reasons = evaluate_gate(gate_stack=_stack(24.99), competitive=comp, rules=RULES)
|
|
assert decision == Decision.APPROVED
|
|
assert reasons
|
|
|
|
|
|
def test_below_break_even_is_blocked():
|
|
comp = CompetitiveSnapshot(buy_box_status=BuyBoxStatus.WON)
|
|
decision, reasons = evaluate_gate(gate_stack=_stack(16.00), competitive=comp, rules=RULES)
|
|
assert decision == Decision.BLOCKED
|
|
assert "break-even" in " ".join(reasons).lower()
|
|
|
|
|
|
def test_below_margin_floor_is_blocked():
|
|
# $18.00 is above break-even ($16.76) but its CM (~6.6%) is under the 25% floor,
|
|
# so it blocks on the margin-floor rule (which is stricter than the MAP guardrail).
|
|
comp = CompetitiveSnapshot(buy_box_status=BuyBoxStatus.WON)
|
|
stack = _stack(18.00)
|
|
decision, reasons = evaluate_gate(gate_stack=stack, competitive=comp, rules=RULES)
|
|
assert decision == Decision.BLOCKED
|
|
assert "floor" in " ".join(reasons).lower()
|
|
# MAP is still computed and available as a downstream guardrail, ~$19.
|
|
assert round(stack.map_floor, 2) == pytest.approx(19.00, abs=0.25)
|
|
|
|
|
|
def test_suppressed_listing_is_blocked_even_if_priced_well():
|
|
comp = CompetitiveSnapshot(buy_box_status=BuyBoxStatus.SUPPRESSED, is_suppressed=True)
|
|
decision, reasons = evaluate_gate(gate_stack=_stack(24.99), competitive=comp, rules=RULES)
|
|
assert decision == Decision.BLOCKED
|
|
assert "suppress" in " ".join(reasons).lower()
|
|
|
|
|
|
def test_lost_buybox_on_price_needs_review():
|
|
comp = CompetitiveSnapshot(buy_box_status=BuyBoxStatus.LOST_PRICE, competitive_median=22.00)
|
|
decision, reasons = evaluate_gate(gate_stack=_stack(24.99), competitive=comp, rules=RULES)
|
|
assert decision == Decision.NEEDS_REVIEW
|
|
|
|
|
|
def test_lost_buybox_on_eligibility_needs_review_not_silent_approval():
|
|
"""Previously fell through every branch and was APPROVED with no mention of the loss."""
|
|
comp = CompetitiveSnapshot(
|
|
buy_box_status=BuyBoxStatus.LOST_ELIGIBILITY, competitive_median=26.00
|
|
)
|
|
decision, reasons = evaluate_gate(gate_stack=_stack(24.99), competitive=comp, rules=RULES)
|
|
assert decision == Decision.NEEDS_REVIEW
|
|
assert "eligibility" in " ".join(reasons).lower()
|